Paprika is a python library that reduces boilerplate. Heavily inspired by Project Lombok.

Overview

A plate filled with paprika spice Image courtesy of Anna Quaglia (Photographer)

GitHub Workflow Status PyPI license Maintainability codecov PyPI Downloads

Paprika

Paprika is a python library that reduces boilerplate. It is heavily inspired by Project Lombok.

Table of Contents

Installation

paprika is available on PyPi.

$ pip install paprika

Usage

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

Features and Examples

Object-oriented decorators

@to_string

The @to_string decorator automatically overrides __str__

Python

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}]"

Python with paprika

@to_string
class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

@equals_and_hashcode

The @equals_and_hashcode decorator automatically overrides __eq__ and __hash__

Python

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))

Python with paprika

@equals_and_hashcode
class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

@data

The @data decorator creates a dataclass by combining @to_string and @equals_and_hashcode and automatically creating a constructor!

Python

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))

Python with paprika

@data
class Person:
    name: str
    age: int

On @data and NonNull

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 ❌

@singleton

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 ✅

Important note on combining @data and @singleton

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.

General utility decorators

@threaded

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)           # 
print(t1.result())  # 42
ThreadPoolExecutor-0_1 woke up after 2s!
ThreadPoolExecutor-0_0 woke up after 5s!

@repeat

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!

Benchmark decorators

timeit

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

@access_counter

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 |
+------------+----------+-----------+

@hotspots

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}

@profile

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.

Error-handling decorators

@catch

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 
    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

Using a custom exception handler

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!

@silent_catch

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!

Contributing

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!

Authors

See also the list of contributors who participated in this project.

License

This project is licensed under the MIT License - see the LICENSE.md file for details

Comments
  • Rename `@serial` to `@pickled`

    Rename `@serial` to `@pickled`

    Summary

    @serial is misleading and may instead be misconstrued to mean JSON serializing. Use @pickled instead

    While we're at it, make hypothesis generate bytes of min_size=100 during randomised testing.

    Test Plan

    Unit tests pass.

    opened by lhl2617 2
  • Tests: Revamp Github Actions

    Tests: Revamp Github Actions

    Carried over from #11--Codecov was unhappy with that one.

    Summary

    • [x] Fetch only latest commit
    • [x] Test on both Python 3.8 and 3.9
    • [x] Test on macos-latest, windows-latest, ubuntu-20.04
    • [x] Use poetry to install dependencies

    Issues

    Unable to get coverage working. Leaving for now to solve later. See #15

    Test Plan

    See checks below.

    opened by lhl2617 2
  • Add `@serial` decorator

    Add `@serial` decorator

    Summary

    • See #10 for context
    • See changes to README.md for example.
    • Added hypothesis for property based testing

    Test plan

    • Added property-based unit tests.
    opened by lhl2617 2
  • Tests: add some tolerance in sleep_after tests

    Tests: add some tolerance in sleep_after tests

    Summary

    sleep_after tests may fail with

    AssertionError: 1.9999143000000004 not greater than or equal to 2
    

    Add 10ms tolerance to the test. Sleeping on non RTOSes are best effort anyway.

    Test Plan

    python -m unittest
    

    Passes on Windows on multiple tries.

    opened by lhl2617 2
  • Tests: Revamp Github Actions

    Tests: Revamp Github Actions

    Summary

    • [x] Fetch only latest commit
    • [x] Test on both Python 3.8 and 3.9
    • [x] Test on macos-latest, windows-latest, ubuntu-20.04
    • [x] Separate unittest and coverage workflows
    • [x] Use poetry to install dependencies

    Test Plan

    See checks below.

    opened by lhl2617 2
  • `pickle` decorator for `class`

    `pickle` decorator for `class`

    Is your feature request related to a problem? Please describe. Often it is required to serialise some data and then deserialise them for use. An example is during machine learning where it would be convenient to persist train/val/test splits.

    Describe the solution you'd like A @pickle decorator for a data class.

    @pickle
    @data
    class Foo:
        x: int
    

    This should override two methods (naming tentative for now)

    def __serialize__(self, file_name: str):
        # Serializes self into a pickled format then save into file_name
        pass
    
    @staticmethod
    def __deserialize__(file_name: str):
        # pickle.load the pickle file at `file_name`.
        pass
    

    Describe alternatives you've considered Doing it manually.

    Additional context Docs: https://docs.python.org/3/library/pickle.html It would be nice to be able to specify the protocol in the decorator as well, e.g. @pickle(protocol=5). Other pickle parameters as per docs are desireable.

    enhancement 
    opened by lhl2617 1
  • Re-structure the package

    Re-structure the package

    Issue

    All decorators are implemented in __init__.py. This will be bad for maintainability once the file gets bigger.

    Mitigation

    We should probably split the decorators into different python files (potentially following the structure from the documentation?) and import those inside __init__.py

    opened by rayanht 1
  • Tests + Coverage

    Tests + Coverage

    Issue

    We're writing code that leverages highly unusual (maybe even suspicious in some instances) python features with no assurance of correctness.

    Mitigation

    Unit tests + automated coverage reports (codecov?). We need to test the behaviour of single decorator as well as common combinations.

    opened by rayanht 0
  • Performance metrics decorators

    Performance metrics decorators

    Feature Description

    We need a family of decorators that allow us to seamlessly benchmark a function with respect to various metrics (number of data accesses, wall time...). Stretch goal: automated hotspot/hot path analysis

    opened by rayanht 0
  • @to_string:  decorated_class.__dict__[attr],

    @to_string: decorated_class.__dict__[attr], "__call__")) KeyError: 'xxx'

    When I decorated my class with @to_string decorator, I got a KeyError. I found that there is a problem with the source code after I debugged.

      attributes = [
          attr
          for attr in dir(self)
          if not attr.startswith("_") and not (
              hasattr(self.__dict__[attr], "__call__") if attr in self.__dict__ else hasattr(
                  decorated_class.__dict__[attr], "__call__"))
      ]
    

    After I print dir(self) and self.__dict__, the attr of dict(self) are more than self.__dict__, thus result in the self.__dict__[attr] KeyError

    bug 
    opened by lifefossil 1
  • Additional decorators

    Additional decorators

    Is your feature request related to a problem? Please describe. N/A

    Describe the solution you'd like A clear and concise description of what you want to happen.

    This blog post has few decorators that might be useful to integrate in paprika.

    Describe alternatives you've considered N/A

    Additional context N/A

    opened by rayanht 0
  • Github Actions: Use poetry + Coverage without breaking codecov

    Github Actions: Use poetry + Coverage without breaking codecov

    The right way to set up a poetry CI env is shown in #14 in .github/workflows/unittest.yml.

    The below was tried for .github/workflows/unittest.yml but does not work due to Codecov failures.

    name: Test and collect coverage
    on: [push, pull_request]
    jobs:
      run:
        runs-on: ubuntu-latest
        env:
          OS: ubuntu-latest
          PYTHON: "3.9"
        steps:
          - uses: actions/[email protected]
            with:
              fetch-depth: 2
          - name: Setup Python
            uses: actions/[email protected]
            with:
              python-version: 3.9
          - name: Setup Poetry
            uses: abatilo/[email protected]
            with:
              poetry-version: 1.1.4
          - name: Install dependencies
            run: poetry install
          - name: Collect coverage
            run: poetry run coverage run -m unittest
          - name: Upload Coverage to Codecov
            uses: codecov/[email protected]
            with:
              verbose: true
    

    The codecov step apparently passes, but the codecov website showed that it failed without much reason.

    My guess is that the previous workflow.yml file has a faulty .env in which the env entry gives 3.9 for python, but the Setup Python step uses a 3.7 version instead. This makes the env mismatch problem persist, but that doesn't make sense to be honest.

    The difference in running the test poetry run coverage run -m unittest vs. coverage run -m unittest may be an issue as well, but running test -f .coverage for the former does not fail.

    opened by lhl2617 2
  • Fix the behaviour of combining @data and @singleton

    Fix the behaviour of combining @data and @singleton

    Describe the bug

    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.

    Expected behavior There should be a single correct way of emulating this behaviour. We should raise an immediately actionable error when they are combined in a way that isn't guaranteed to function.

    opened by rayanht 0
Releases(v1.3.0)
Owner
Rayan Hatout
CS student at Imperial College London and Software Engineer.
Rayan Hatout
Your missing PO formatter and linter

pofmt Your missing PO formatter and linter Features Wrap msgid and msgstr with a constant max width. Can act as a pre-commit hook. Display lint errors

Frost Ming 5 Mar 22, 2022
Extract continuous and discrete relaxation spectra from G(t)

pyReSpect-time Extract continuous and discrete relaxation spectra from stress relaxation modulus G(t). The papers which describe the method and test c

3 Nov 03, 2022
ARK sõidueksami Matrixi bot

ARK Sõidueksami bot Küsib ARK-i lehelt uusimad eksami ajad ja saadab sõnumi Matrixi kanali Dev setup Linux python3 -m venv venv source venv/bin/activa

Arti Zirk 3 Jun 15, 2021
NFT generator for Solana!

Solseum NFT Generator for Solana! Check this guide here! Creating your randomized uniques NFTs, getting rarity information and displaying it on a webp

Solseum™ VR NFTs 145 Dec 30, 2022
Graphene Metanode is a locally hosted node for one account and several trading pairs, which uses minimal RAM resources.

Graphene Metanode is a locally hosted node for one account and several trading pairs, which uses minimal RAM resources. It provides the necessary user stream data and order book data for trading in a

litepresence 5 May 08, 2022
ColabFold / AlphaFold2_advanced on your local PC (or macOS)

LocalColabFold ColabFold / AlphaFold2_advanced on your local PC (or macOS) Installation For Linux Make sure curl and wget commands are already install

Yoshitaka Moriwaki 207 Dec 22, 2022
🎴 LearnQuick is a flashcard application that you can study with decks and cards.

🎴 LearnQuick is a flashcard application that you can study with decks and cards. The main function of the application is to show the front sides of the created cards to the user and ask them to guess

Mehmet Güdük 7 Aug 21, 2022
Auto check in via GitHub Actions

因为本人毕业离校,本项目交由在校的@hfut-xyc同学接手,请访问hfut-xyc/hfut_auto_check-in获得最新的脚本 本项目遵从GPLv2协定,Copyright (C) 2021, Fw[a]rd 免责声明 根据GPL协定,我、本项目的作者,不会对您使用这个脚本带来的任何后果

Fw[a]rd 3 Jun 27, 2021
Audio2Face - a project that transforms audio to blendshape weights,and drives the digital human,xiaomei,in UE project

Audio2Face - a project that transforms audio to blendshape weights,and drives the digital human,xiaomei,in UE project

FACEGOOD 732 Jan 08, 2023
Fork of pathlib aiming to support the full stdlib Python API.

pathlib2 Fork of pathlib aiming to support the full stdlib Python API. The old pathlib module on bitbucket is in bugfix-only mode. The goal of pathlib

Jazzband 73 Dec 23, 2022
A Python simple Dice Simulator just for fun

Dice Simulator 🎲 A Simple Python Dice Simulator 🧩 🎮 💭 Description: That program make your RPG session more easy and simple. Roll the dice never be

Lauro Brant 17 May 14, 2022
a url shortener with fastapi and tortoise-orm

fastapi-tortoise-orm-url-shortener a url shortener with fastapi and tortoise-orm

19 Aug 12, 2022
Script that creates graphical representations of Julia an Mandelbrot sets.

Julia and Mandelbrot Picture Maker This simple functions create simple plots of the Julia and Mandelbrot sets. The Julia set require the important par

Juan Riera Gomez 1 Jan 10, 2022
Linux Security and Monitoring Scripts

Linux Security and Monitoring Scripts These are a collection of security and monitoring scripts you can use to monitor your Linux installation for sec

Andre Pawlowski 65 Aug 27, 2022
peace-performance (Rust) binding for python. To calculate star ratings and performance points for all osu! gamemodes

peace-performance-python Fast, To calculate star ratings and performance points for all osu! gamemodes peace-performance (Rust) binding for python bas

9 Sep 19, 2022
Snek-test - An operating system kernel made in python and assembly

pythonOS An operating system kernel made in python and assembly Wait what? It us

TechStudent10 2 Jan 25, 2022
This is a fork of the BakeTool with some improvements that I did to have better workflow.

blender-bake-tool This is a fork of the BakeTool with some improvements that I did to have better workflow. 99.99% of work was done by BakeTool team.

Acvarium 3 Oct 04, 2022
A supercharged version of paperless: scan, index and archive all your physical documents

Paperless-ng Paperless (click me) is an application by Daniel Quinn and contributors that indexes your scanned documents and allows you to easily sear

Jonas Winkler 5.3k Jan 09, 2023
Find all solutions to SUBSET-SUM, including negative, positive, and repeating numbers

subsetsum The subsetsum Python module can enumerate all combinations within a list of integers which sums to a specific value. It works for both negat

Trevor Phillips 9 May 27, 2022
Graphsignal Logger

Graphsignal Logger Overview Graphsignal is an observability platform for monitoring and troubleshooting production machine learning applications. It h

Graphsignal 143 Dec 05, 2022