Skip to main content

biwrap

Yet simple util to make wrapper with optional arguments

Installation

Master branch

pip install git+https://github.com/ferrine/biwrap

Latest release

pip install biwrap

Problem outlined

Readability and transparent implementation is important. Wrappers is an advanced topic in programming and making them readable in some cases is difficult. The particular case is parametrizable wrapper. It can either be called with or without arguments. Implementation to handle this case is often tricky and looks weird (see SO thread). This package solves the problem and provides with simple and generic solution.

API

A minimal snippet to apply it to your problem.

import biwrap

@biwrap.biwrap
def wrapper(fn, arg1='default', arg2='default'):
    def wrapped(*fn_args, **fn_kwargs):
        ...  # depends on `arg1`, `arg2`
    return wrapped

@wrapper  # use defaults
def func1(a, b):
    ...

@wrapper(arg1='non default')  # change defaults
def func2(a, b):
    ...

Example

Let’s discuss a case when it is needed to put functions into a registry. Some functions can have alias names.

Naive Solution

def register(alias=None):
    def inner(fn):
        if fn.__name__ not in register.storage:
            register.storage[fn.__name__] = fn
        elif register.storage[fn.__name__] is not fn:
            raise KeyError('{} is already in storage'.format(fn.__name__))
        if alias is not None and alias not in register.storage:
            register.storage[alias] = fn
        elif alias is not None:
            raise KeyError('{} is already in storage'.format(alias))
        return fn
    return inner
register.storage = {}


@register()
def f1(a):
    return a

print(register.storage)
#> {'f1': <function f1 at 0x11ff519d8>}


@register(alias='fn3')
def f2(a):
    return a

print(register.storage)
#> {'f1': <function f1 at 0x11ff519d8>, 'f2': <function f2 at 0x10a87d0d0>, 'fn3': <function f2 at 0x10a87d0d0>}

Analysis

The above example shows redundancy in

  • decorator definition has double nesting (double def)

  • usage requires trailing parenthesis @register() even in case we do not use optional argument

More readable code should avoid these two points and look like:

def register(fn, alias=None):
    ...

@register
def f1(a):
    return a

@register(alias='fn3')  # <- (1)
def f2(a):
    return a

Naive implementation of the above API won’t work. Line marked above as (1) will fail as first argument fn is not passed. But we want the output to be the same.

Better solution

import biwrap

@biwrap.biwrap
def register(fn, alias=None):
    if fn.__name__ not in register.storage:
        register.storage[fn.__name__] = fn
    elif register.storage[fn.__name__] is not fn:
        raise KeyError('{} is already in storage'.format(fn.__name__))
    if alias is not None and alias not in register.storage:
        register.storage[alias] = fn
    elif alias is not None:
        raise KeyError('{} is already in storage'.format(alias))
    return fn
register.storage = {}

@register
def f1(a):
    return a

print(register.storage)
#> {'f1': <function f1 at 0x10f45a048>}

@register(alias='fn3')
def f2(a):
    return a

print(register.storage)
#> {'f1': <function f1 at 0x10f45a048>, 'f2': <function f2 at 0x10f45a488>, 'fn3': <function f2 at 0x10f45a488>}

Functionality Overview

Some corner cases may exist and custom coding can create a boilerplate for each usecase (see this SO thread). This package takes the best and implements yet simple but generic solution to resolve them all(?).

Setup

Let’s take a simple wrapper as an example. It will print hi or bye depending on parametrization, default is hi.

import biwrap

@biwrap.biwrap
def hiwrap(fn, hi=True):
    def new(*args, **kwargs):
        if hi:
            print('hi')
        else:
            print('bye')
        return fn(*args, **kwargs)
    return new

Cases

Function wrapping

Defined wrapper can be used in both ways

@hiwrap
def fn(n):
    print(n)
fn(1)
#> hi
#> 1

@hiwrap(hi=False)
def fn(n):
    print(n)
fn(1)
#> bye
#> 1

Bound method wrapper

biwrap also works for bound methods. As seen in SO thread this can be a problem as first positional argument is self instead of a function.

class W:
    def __init__(self, n):
        self.n = n

    @biwrap.biwrap
    def wrap(self, fn, text='hi'):
        def wrapped(*args, **kwargs):
            for _ in range(self.n):
                print(text)
            return fn(*args, **kwargs)
        return wrapped
wr = W(3)

@wr.wrap
def fn(n):
    print(n)

fn(1)
#> hi
#> hi
#> hi
#> 1

@wr.wrap(text='bye')
def fn(n):
    print(n)

wr.n = 2
fn(2)
#> bye
#> bye
#> 2

Class methods / properties wrapping

Implementation deals with these cases as well

class O:
    def __init__(self, n):
        self.n = n

    @classmethod
    @hiwrap
    def fn(cls, n):
        print(n)

    @property
    @hiwrap(hi=False)
    def num(self):
        return self.n


o = O(2)
o.fn(1)
#> hi
#> 1
print(o.num)
#> bye
#> 2

Wrapper as a function

Function like call is OK too

def fn(n):
    print(n)

fn = hiwrap(fn, hi=False)
fn(1)
#> bye
#> 1

Metadata

Release files for biwrap 0.1.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for biwrap 0.1.6
File Size Uploaded Provenance Actions
biwrap-0.1.6.tar.gz 3.9 kB Without provenance Details for biwrap-0.1.6.tar.gz

Release files / biwrap-0.1.6.tar.gz

Download URL biwrap-0.1.6.tar.gz
Size 3.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f6346f9973727d101258cf03e5c343197b8fc50a8f7fe80353e808581a021450
BLAKE2b-256 checksum
How to use checksums
ac5478d4d5459d7385f90908185641647ff2e8ed4a4b5c239976f78e26888679
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No

Security for biwrap 0.1.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page