Skip to main content

Keyword argument tracing !!

Project description

starstar ✨ ✨

pypi tests docs License

Finally! Variable keyword tracing in Python.

Because this makes me sad:

def main(**kw):
    function_with_a_bunchhhh_of_arguments(**kw)  # I only want to pass some of **kw !!
    another_function_with_a_bunchhhh_of_arguments(**kw)  # and put the other half here !!!

# hmmm let's see what can I pass to this function...
help(main)  # main(**kw)
# HALP????? aljdsflaksjdflkasjd

😖😭😭

And why can't we have: 🧞‍♀️ 🧚🏻‍♀️ ✨ ✨

import starstar

def function_a(a=1, b=2, c=3): ...
def function_b(x=8, y=9, z=10): ...

@starstar.traceto(function_a, function_b)
def main(**kw):
    kw_a, kw_b = starstar.divide(kw, function_a, function_b)
    function_a(**kw_a)  # gets: a, b, c
    function_b(**kw_b)  # gets: x, y, z

# hmmm let's see what can I pass to this function...
help(main)  # main(a=1, b=2, c=3, x=8, y=9, z=10)
# yayyyy!!!

😇🥰🌈

Do you:

  • dislike repeating function arguments and their default values and therefore use **kwargs a lot?
  • sometimes need to pass **kwargs down to multiple functions, but hate that it requires enumerating all but one of the functions parameters?
  • wish that Python could look inside itself and figure it out for you?

starstar attempts to bridge the gap between nice, clean, and concise code (DRY ! EVER! <3) while maintaining informative introspectability of your functions.

It also tries to do it as efficiently as it can internally by minimizing the amount of overhead added to the actual function calls and trying to use wrapper functions sparingly.

It can:

  • look at function signatures and uses the parameters described to sort out kwargs into separate dictionaries for each function. (divide(kw, *funcs))
  • modify a function's signature to include parameters from other functions that it wraps and sends its **kwargs to (traceto(*funcs))
  • perform functools.wraps, while also preserving any arguments from the wrapper function in the signature (wraps(func)(wrapper))

I really hope you find this useful! Please feel free to open issues if you have requests/suggestions cuz I want you to love this too!

Install

pip install starstar

Usage

Pass arguments to multiple functions!

We have a function that wants to pass arguments to two different functions without having to enumerate those arguments in that function.

import starstar

def func_a(a=None, b=None, c=None):
    return a, b, c

def func_b(d=None, e=None, f=None):
    return d, e, f

def main(**kw):
    kw_a, kw_b = starstar.divide(kw, func_a, func_b)
    func_a(**kw_a)
    func_b(**kw_b)

Pass arguments to multiple functions down multiple levels!

Here we're passing it down two levels so we break up arguments for func_a and func_b into the first dictionary and func_d into the second.

def func_c(**kw):
    kw_a, kw_b = starstar.divide(kw, func_a, func_b)
    func_a(**kw_a)
    func_b(**kw_b)

def func_d(g=None, h=None, i=None):
    return g, h, i

def main(**kw):
    kw_c, kw_d = starstar.divide(kw, (func_a, func_b), func_d)  # combine multiple functions into one kw dict
    func_c(**kw_c)
    func_d(**kw_d)

But we can even make this one step easier! Here we are modifying the signature of func_c to say that its arguments are sent to func_a and func_b.

@starstar.traceto(func_a, func_b)
def func_c(**kw):
    kw_a, kw_b = starstar.divide(kw, func_a, func_b)
    func_a(**kw_a)
    func_b(**kw_b)

@starstar.traceto(func_c, func_d)
def main(**kw):
    kw_c, kw_d = starstar.divide(kw, func_c, func_d)
    func_c(**kw_c)
    func_d(**kw_d)

Which results in these signatures:

import inspect
print(inspect.signature(func_c))
print(inspect.signature(main))
# (a=None, b=None, c=None, d=None, e=None, f=None)
# (a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None, i=None)

Bonus

functools.wraps, but better

Builtin functools.wraps doesn't consider the arguments to inner so its wrapped signature doesn't know about them which can be misleading for any tools that rely on accurate signatures.

import functools

def deco(func):
    @functools.wraps(func)
    def inner(q, *a, **kw):
        return q, func(*a, **kw)
    return inner

@deco
def asdf(x, y, z):
    pass

import inspect
print(inspect.signature(asdf))  # (x, y, z)

But now it can!

import starstar

def deco(func):
    @starstar.wraps(func)
    def inner(q, *a, **kw):
        return q, func(*a, **kw)
    return inner

@deco
def asdf(x, y, z):
    pass

import inspect
print(inspect.signature(asdf))  # (q, x, y, z)

And you can also skip certain positional or named arguments if the wrapper already provides them.

import starstar

def deco(func):
    @starstar.wraps(func, skip_n=2, skip_args=('blah',))
    def inner(q, *a, **kw):
        return q, func(1, 2, *a, blah=17, **kw)
    return inner

Overriding function defaults

Say we want to change the default arguments for a function (e.g. we want to offload that configuration to a yaml config file).

import starstar

@starstar.defaults
def func_x(x, y=6):
    return x, y

import inspect
print(inspect.signature(func_x))  # <Signature (x, y=6)>

assert func_x(5) == 11
func_x.update(y=7)
assert func_x(5) == 12

import inspect
print(inspect.signature(func_x))  # <Signature (x, y=17)>
import yaml

with open(config_file, 'r') as f:
    config = yaml.load(f)

func_x.update(**(config.get('func_x') or {}))

For the Future

  • tracing pos args? After some thought - this seems troublesome because I'm not sure how we'd deal with name conflicts between kwargs.
  • traceto allow skipping?
  • have a more concrete plan around keyword name collisions.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

starstar-0.6.1.tar.gz (33.8 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page