starstar


Namestarstar JSON
Version 0.6.1 PyPI version JSON
download
home_pagehttps://github.com/beasteers/starstar
SummaryKeyword argument tracing !!
upload_time2023-10-02 23:04:40
maintainer
docs_urlNone
authorBea Steers
requires_python
licenseMIT License
keywords keyword kwargs kw star function arguments signature wrap trace
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # starstar  ✨ ✨

[![pypi](https://badge.fury.io/py/starstar.svg)](https://pypi.python.org/pypi/starstar/)
![tests](https://github.com/beasteers/starstar/actions/workflows/ci.yml/badge.svg)
[![docs](https://readthedocs.org/projects/starstar/badge/?version=latest)](http://starstar.readthedocs.io/?badge=latest)
[![License](https://img.shields.io/pypi/l/starstar.svg)](https://github.com/beasteers/starstar/blob/main/LICENSE.md)

Finally! Variable keyword tracing in Python. 

Because this makes me sad:
```python
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: 🧞‍♀️ 🧚🏻‍♀️ ✨ ✨ 
```python
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

```bash
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.
```python
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.
```python
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`.
```python
@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:
```python
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.
```python
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!
```python
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.
```python
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). 

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

```python
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.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/beasteers/starstar",
    "name": "starstar",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "keyword kwargs kw star function arguments signature wrap trace",
    "author": "Bea Steers",
    "author_email": "bea.steers@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/18/c9/71b7ba4f74159eb9ee40d6ad7d2d539a5f3cf37a303011e94735663bafbe/starstar-0.6.1.tar.gz",
    "platform": null,
    "description": "# starstar  \u2728 \u2728\n\n[![pypi](https://badge.fury.io/py/starstar.svg)](https://pypi.python.org/pypi/starstar/)\n![tests](https://github.com/beasteers/starstar/actions/workflows/ci.yml/badge.svg)\n[![docs](https://readthedocs.org/projects/starstar/badge/?version=latest)](http://starstar.readthedocs.io/?badge=latest)\n[![License](https://img.shields.io/pypi/l/starstar.svg)](https://github.com/beasteers/starstar/blob/main/LICENSE.md)\n\nFinally! Variable keyword tracing in Python. \n\nBecause this makes me sad:\n```python\ndef main(**kw):\n    function_with_a_bunchhhh_of_arguments(**kw)  # I only want to pass some of **kw !!\n    another_function_with_a_bunchhhh_of_arguments(**kw)  # and put the other half here !!!\n\n# hmmm let's see what can I pass to this function...\nhelp(main)  # main(**kw)\n# HALP????? aljdsflaksjdflkasjd\n```\n\ud83d\ude16\ud83d\ude2d\ud83d\ude2d\n\nAnd why can't we have: \ud83e\uddde\u200d\u2640\ufe0f \ud83e\uddda\ud83c\udffb\u200d\u2640\ufe0f \u2728 \u2728 \n```python\nimport starstar\n\ndef function_a(a=1, b=2, c=3): ...\ndef function_b(x=8, y=9, z=10): ...\n\n@starstar.traceto(function_a, function_b)\ndef main(**kw):\n    kw_a, kw_b = starstar.divide(kw, function_a, function_b)\n    function_a(**kw_a)  # gets: a, b, c\n    function_b(**kw_b)  # gets: x, y, z\n\n# hmmm let's see what can I pass to this function...\nhelp(main)  # main(a=1, b=2, c=3, x=8, y=9, z=10)\n# yayyyy!!!\n```\n\ud83d\ude07\ud83e\udd70\ud83c\udf08\n\nDo you:\n - dislike repeating function arguments and their default values and therefore use `**kwargs` a lot?\n - sometimes need to pass `**kwargs` down to multiple functions, but hate that it requires enumerating all but one of the functions parameters?\n - wish that Python could look inside itself and figure it out for you?\n\n`starstar` attempts to bridge the gap between nice, clean, and concise code (DRY ! EVER! <3) while maintaining informative introspectability of your functions. \n\nIt 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.\n\nIt can: \n - look at function signatures and uses the parameters described to sort out kwargs into separate dictionaries for each function. (`divide(kw, *funcs)`)\n - modify a function's signature to include parameters from other functions that it wraps and sends its `**kwargs` to (`traceto(*funcs)`)\n - perform `functools.wraps`, while also preserving any arguments from the wrapper function in the signature (`wraps(func)(wrapper)`)\n\nI 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!\n\n## Install\n\n```bash\npip install starstar\n```\n\n## Usage\n\n### Pass arguments to multiple functions!\nWe have a function that wants to pass arguments to two different functions without having to enumerate those arguments in that function.\n```python\nimport starstar\n\ndef func_a(a=None, b=None, c=None):\n    return a, b, c\n\ndef func_b(d=None, e=None, f=None):\n    return d, e, f\n\ndef main(**kw):\n    kw_a, kw_b = starstar.divide(kw, func_a, func_b)\n    func_a(**kw_a)\n    func_b(**kw_b)\n```\n\n\n\n### Pass arguments to multiple functions down multiple levels!\nHere 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.\n```python\ndef func_c(**kw):\n    kw_a, kw_b = starstar.divide(kw, func_a, func_b)\n    func_a(**kw_a)\n    func_b(**kw_b)\n\ndef func_d(g=None, h=None, i=None):\n    return g, h, i\n\ndef main(**kw):\n    kw_c, kw_d = starstar.divide(kw, (func_a, func_b), func_d)  # combine multiple functions into one kw dict\n    func_c(**kw_c)\n    func_d(**kw_d)\n```\n\nBut 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`.\n```python\n@starstar.traceto(func_a, func_b)\ndef func_c(**kw):\n    kw_a, kw_b = starstar.divide(kw, func_a, func_b)\n    func_a(**kw_a)\n    func_b(**kw_b)\n\n@starstar.traceto(func_c, func_d)\ndef main(**kw):\n    kw_c, kw_d = starstar.divide(kw, func_c, func_d)\n    func_c(**kw_c)\n    func_d(**kw_d)\n```\nWhich results in these signatures:\n```python\nimport inspect\nprint(inspect.signature(func_c))\nprint(inspect.signature(main))\n# (a=None, b=None, c=None, d=None, e=None, f=None)\n# (a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None, i=None)\n```\n\n### Bonus\n#### `functools.wraps`, but better\nBuiltin `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.\n```python\nimport functools\n\ndef deco(func):\n    @functools.wraps(func)\n    def inner(q, *a, **kw):\n        return q, func(*a, **kw)\n    return inner\n\n@deco\ndef asdf(x, y, z):\n    pass\n\nimport inspect\nprint(inspect.signature(asdf))  # (x, y, z)\n```\n\nBut now it can!\n```python\nimport starstar\n\ndef deco(func):\n    @starstar.wraps(func)\n    def inner(q, *a, **kw):\n        return q, func(*a, **kw)\n    return inner\n\n@deco\ndef asdf(x, y, z):\n    pass\n\nimport inspect\nprint(inspect.signature(asdf))  # (q, x, y, z)\n```\n\nAnd you can also skip certain positional or named arguments if the wrapper already provides them.\n```python\nimport starstar\n\ndef deco(func):\n    @starstar.wraps(func, skip_n=2, skip_args=('blah',))\n    def inner(q, *a, **kw):\n        return q, func(1, 2, *a, blah=17, **kw)\n    return inner\n```\n\n#### Overriding function defaults\nSay we want to change the default arguments for a function (e.g. we want to offload that configuration to a yaml config file). \n\n```python\nimport starstar\n\n@starstar.defaults\ndef func_x(x, y=6):\n    return x, y\n\nimport inspect\nprint(inspect.signature(func_x))  # <Signature (x, y=6)>\n\nassert func_x(5) == 11\nfunc_x.update(y=7)\nassert func_x(5) == 12\n\nimport inspect\nprint(inspect.signature(func_x))  # <Signature (x, y=17)>\n```\n\n```python\nimport yaml\n\nwith open(config_file, 'r') as f:\n    config = yaml.load(f)\n\nfunc_x.update(**(config.get('func_x') or {}))\n```\n\n## For the Future\n - tracing pos args? After some thought - this seems troublesome because I'm not sure how we'd deal with name conflicts between kwargs.\n - `traceto` allow skipping?\n - have a more concrete plan around keyword name collisions.\n",
    "bugtrack_url": null,
    "license": "MIT License",
    "summary": "Keyword argument tracing !!",
    "version": "0.6.1",
    "project_urls": {
        "Homepage": "https://github.com/beasteers/starstar"
    },
    "split_keywords": [
        "keyword",
        "kwargs",
        "kw",
        "star",
        "function",
        "arguments",
        "signature",
        "wrap",
        "trace"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "18c971b7ba4f74159eb9ee40d6ad7d2d539a5f3cf37a303011e94735663bafbe",
                "md5": "8b2248a844d763d9f1950ca170611134",
                "sha256": "b25403c1e4ec09cb91b352832a779af1b39611713f3f20e7fdcd003c17c356c0"
            },
            "downloads": -1,
            "filename": "starstar-0.6.1.tar.gz",
            "has_sig": false,
            "md5_digest": "8b2248a844d763d9f1950ca170611134",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 33838,
            "upload_time": "2023-10-02T23:04:40",
            "upload_time_iso_8601": "2023-10-02T23:04:40.978261Z",
            "url": "https://files.pythonhosted.org/packages/18/c9/71b7ba4f74159eb9ee40d6ad7d2d539a5f3cf37a303011e94735663bafbe/starstar-0.6.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-10-02 23:04:40",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "beasteers",
    "github_project": "starstar",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "starstar"
}
        
Elapsed time: 0.83406s