Python Dynamic Default Arguments
======
![Build wheel](https://github.com/inspiros/dynamic-default-args/actions/workflows/build_wheels.yml/badge.svg)
![PyPI](https://img.shields.io/pypi/v/dynamic-default-args)
![GitHub](https://img.shields.io/github/license/inspiros/dynamic-default-args)
This package provides facilities to make default arguments of Python's functions dynamic (in an elegant manner).
## Context
The package solves a problem that was also mentioned in
this [stackoverflow thread](https://stackoverflow.com/questions/16960469/dynamic-default-arguments-in-python-functions).
The common approach is to define a function that retrieves the value of the _default_ argument stored somewhere:
```python
class _empty(type):
pass # placeholder
B = 'path/to/heaven'
def get_default_b():
# function that retrieves the 'default' value
return B
def foo(a, b=_empty):
if b is _empty:
b = get_default_b()
send_to(a, destination=b) # do something
def main():
global B
B = 'path/to/hell'
foo('Putin')
```
The old standard way is ok, but we should be aware of numbers of function calls when there are many arguments to be made
dynamically default.
But the point is, it doesn't look nice.
This module's solution limits to a single wrapper function, which is `compile`d from string to minimize overheads on
runtime.
## Requirements
- Python 3
## Installation
[dynamic-default-args](https://pypi.org/project/dynamic-default-args/) is available on PyPI, this is a pure Python
package.
```bash
pip install dynamic-default-args
```
## Usage
This package provides two components:
- `named_default`: A object that has a name and contains a value.
This is a singleton-like object, which means any initialization with the same name will return the first one with the
same registered name.
- `dynamic_default_args`: A function decorator for substituting any given `named_default` with its value when function
is called.
### Creating a `named_default`:
There are 3 ways to initialize a `named_default`:
- Pass a pair of positional arguments `named_default([name], [value])`
- Pass the two keywords `named_default(name=[name], value=[value])`
- Pass a single keyword argument `named_default([name]=[value])`.
```python
from dynamic_default_args import named_default
# method 1
x = named_default('x', 1)
# method 2
y = named_default(name='y', value=2)
# method 3
z = named_default(z=3)
```
It is not necessary to keep the reference of this object as you can always recover them when calling `named_default`
again with the same name. New value passed to the constructor will be ignored.
```python
from dynamic_default_args import named_default
print(named_default('x').value)
named_default('x').value = 1e-3
```
Trying to access an unregistered name will raise Exception.
```python
from dynamic_default_args import named_default
print(named_default('an_unregistered_name').value)
# ValueError: an_unregistered_name has not been registered.
```
### Decorating functions with `dynamic_default_args`:
Here is an example in [`example.py`](examples/example.py) on Python 3.8+:
```python title=foo.py
from dynamic_default_args import dynamic_default_args, named_default
# Note that even non-dynamic default args can be formatted because
# both are saved for populating positional-only defaults args
@dynamic_default_args(format_doc=True)
def foo(a0,
a1=named_default(a1=5),
a2=3,
/,
a3=named_default(a3=slice(0, 3)),
a4=-1,
*a5,
a6=None,
a7=named_default(a7='python'),
**a8):
"""
A Foo function that has dynamic default arguments.
Args:
a0: Required Positional-only argument a0.
a1: Positional-only argument a1. Dynamically defaults to a0={a1}.
a2: Positional-only argument a1. Defaults to {a2}.
a3: Positional-or-keyword argument a2. Dynamically defaults to a3={a3}.
a4: Positional-or-keyword argument a4. Defaults to {a4}
*a5: Varargs a5.
a6: Keyword-only argument a5. Defaults to {a6}.
a7: Keyword-only argument a6. Dynamically defaults to {a7}.
**a8: Varkeywords a8.
"""
print(f'Called with: a0={a0}, a1={a1}, a2={a2}, a3={a3}, '
f'a4={a4}, a5={a5}, a6={a6}, a7={a7}, a8={a8}')
# test output:
foo(0)
# Called with: a0=0, a1=5, a2=3, a3=slice(0, 3, None), a4=-1, a5=(), a6=None, a7=python, a8={}
```
#### How it works?
Internally, the auto generated wrapper with similar signature for this function will be (without formatting):
```python
def wrapper(a0, a1=a1_, a2=a2_, a3=a3_, a4=a4_, *a5, a6=a6_, a7=a7_, **a8):
func(a0,
a1._value if isinstance(a1, default) else a1,
a2,
a3._value if isinstance(a3, default) else a3,
a4,
*a5,
a6=a6,
a7=a7._value if isinstance(a7, default) else a7,
**a8)
```
whose defaults are set to those of `func`(`=foo`), but the contained `named_default`s will be type checked and have
their values forwarded instead.
How the arguments are forwared depend on the type of arguments:
- **Positional-only**: with its name, e.g.`a0`, `a1`, `a2`
- **Keyword-or-position**: with its name, e.g. `a3`, `a4`
- **Varargs**: with an asterisk operator, e.g. `*a5`
- **Keyword-only**: with its name as key, e.g. `a6=a6`, `a7=a7`
- **Varkeywords**: with double asterick operator, e.g. `**a8`
**Note:** _For those who don't know, the type of argument depends on its position relative to the 3 syntax's `/`, `*`,
and `**`:_
```python
def f(po0, ___, /, pok0, ____, *args, kw0, kw1, _____, **kwargs):
# --------- ----------- | ---------------- |
# | | | | |
# | Positional - | | Varkeywords
# | or -keyword | Keyword - only
# Positional - only Varargs
...
```
**Note:** _The aliases `wrapper, func, default` are assured to be different from the original arguments' names._
#### Docstring formatting
By configuring `@named_default_args(format_doc=True)` (which is the default behavior), the decorator will try to bind
the default values of arguments with names defined in format keys `{[argument_name]}` in the docstring.
Any modification to the `value` property of `named_default` will update the docstring with an event.
```python
named_default('a1').value *= 2
named_default('a3').value = range(10)
named_default('a7').value = 'rust'
help(foo)
```
Output: _(even normal default arguments will be formatted)_
```
foo(a0, a1=10, a2=3, /, a3=range(0, 10), a4=-1, *a5, a6=None, a7='rust', **a8)
A Foo function that has dynamic default arguments.
Args:
a0: Required Positional-only argument a0.
a1: Positional-only argument a1. Dynamically defaults to a0=10.
a2: Positional-only argument a1. Defaults to 3.
a3: Positional-or-keyword argument a2. Dynamically defaults to a3=range(0, 10).
a4: Positional-or-keyword argument a4. Defaults to -1
*a5: Varargs a5.
a6: Keyword-only argument a5. Defaults to None.
a7: Keyword-only argument a6. Dynamically defaults to rust.
**a8: Varkeywords a8.
```
#### Binding
The `named_default` object will emit an event to all registered listeners when its `value` property is modified.
You can register your own handler by calling `.connect` method:
```python
from dynamic_default_args import named_default
variable = named_default('my_variable', None)
def on_variable_changed(value):
print(f'Changed to {value}')
variable.connect(on_variable_changed)
# modifying the slot has no effect
# but accessing its value is much faster this way
variable._value = 'this doesn\'t work'
variable.value = 'this works!'
# Changed to this works!
```
### Limitations
This solution relies on function introspection provided by the `inspect` module, which does not work on built-in
functions (including C/C++ extensions).
However, you can wrap them with your own Python function.
For **Cython** users, a `def` or `cpdef` (which might be inspected incorrectly) function defined in `.pyx` files can be
decorated by setting `binding=True`.
```cython
import cython
from dynamic_default_args import dynamic_default_args, named_default
@dynamic_default_args(format_doc=True)
@cython.binding(True)
def add(x: float = named_default(x=0.),
y: float = named_default(y=0.)):
"""``cython.binding`` will add docstring to the wrapped function,
so we can format it later.
Args:
x: First argument, dynamically defaults to {x}
y: Second argument, dynamically defaults to {y}
Returns:
The sum of x and y
"""
return x + y
```
Also, it is clear that decorators are not lazily initialized.
**Further improvements:**
Modifying the `func.__defaults__` should be more performant.
### License
The code is released under MIT-0 license. See [`LICENSE.txt`](LICENSE.txt) for details.
Feel free to do anything, I would be surprised if anyone does use this 😐.
Raw data
{
"_id": null,
"home_page": "https://github.com/inspiros/dynamic-default-args",
"name": "dynamic-default-args",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3",
"maintainer_email": "",
"keywords": "dynamic-default-args",
"author": "Hoang-Nhat Tran (inspiros)",
"author_email": "hnhat.tran@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/89/96/2d3da1a1e04bae25926f98bfc3247737880e46ccbff5a2ac539abcd8f9bf/dynamic-default-args-0.0.6.tar.gz",
"platform": null,
"description": "Python Dynamic Default Arguments\r\n======\r\n![Build wheel](https://github.com/inspiros/dynamic-default-args/actions/workflows/build_wheels.yml/badge.svg)\r\n![PyPI](https://img.shields.io/pypi/v/dynamic-default-args)\r\n![GitHub](https://img.shields.io/github/license/inspiros/dynamic-default-args)\r\n\r\nThis package provides facilities to make default arguments of Python's functions dynamic (in an elegant manner).\r\n\r\n## Context\r\n\r\nThe package solves a problem that was also mentioned in\r\nthis [stackoverflow thread](https://stackoverflow.com/questions/16960469/dynamic-default-arguments-in-python-functions).\r\n\r\nThe common approach is to define a function that retrieves the value of the _default_ argument stored somewhere:\r\n\r\n```python\r\nclass _empty(type):\r\n pass # placeholder\r\n\r\n\r\nB = 'path/to/heaven'\r\n\r\n\r\ndef get_default_b():\r\n # function that retrieves the 'default' value\r\n return B\r\n\r\n\r\ndef foo(a, b=_empty):\r\n if b is _empty:\r\n b = get_default_b()\r\n send_to(a, destination=b) # do something\r\n\r\n\r\ndef main():\r\n global B\r\n B = 'path/to/hell'\r\n foo('Putin')\r\n```\r\n\r\nThe old standard way is ok, but we should be aware of numbers of function calls when there are many arguments to be made\r\ndynamically default.\r\nBut the point is, it doesn't look nice.\r\n\r\nThis module's solution limits to a single wrapper function, which is `compile`d from string to minimize overheads on\r\nruntime.\r\n\r\n## Requirements\r\n\r\n- Python 3\r\n\r\n## Installation\r\n\r\n[dynamic-default-args](https://pypi.org/project/dynamic-default-args/) is available on PyPI, this is a pure Python\r\npackage.\r\n\r\n```bash\r\npip install dynamic-default-args\r\n```\r\n\r\n## Usage\r\n\r\nThis package provides two components:\r\n\r\n- `named_default`: A object that has a name and contains a value.\r\n This is a singleton-like object, which means any initialization with the same name will return the first one with the\r\n same registered name.\r\n- `dynamic_default_args`: A function decorator for substituting any given `named_default` with its value when function\r\n is called.\r\n\r\n### Creating a `named_default`:\r\n\r\nThere are 3 ways to initialize a `named_default`:\r\n\r\n- Pass a pair of positional arguments `named_default([name], [value])`\r\n- Pass the two keywords `named_default(name=[name], value=[value])`\r\n- Pass a single keyword argument `named_default([name]=[value])`.\r\n\r\n```python\r\nfrom dynamic_default_args import named_default\r\n\r\n# method 1\r\nx = named_default('x', 1)\r\n# method 2\r\ny = named_default(name='y', value=2)\r\n# method 3\r\nz = named_default(z=3)\r\n```\r\n\r\nIt is not necessary to keep the reference of this object as you can always recover them when calling `named_default`\r\nagain with the same name. New value passed to the constructor will be ignored.\r\n\r\n```python\r\nfrom dynamic_default_args import named_default\r\n\r\nprint(named_default('x').value)\r\nnamed_default('x').value = 1e-3\r\n```\r\n\r\nTrying to access an unregistered name will raise Exception.\r\n\r\n```python\r\nfrom dynamic_default_args import named_default\r\n\r\nprint(named_default('an_unregistered_name').value)\r\n# ValueError: an_unregistered_name has not been registered.\r\n```\r\n\r\n### Decorating functions with `dynamic_default_args`:\r\n\r\nHere is an example in [`example.py`](examples/example.py) on Python 3.8+:\r\n\r\n```python title=foo.py\r\nfrom dynamic_default_args import dynamic_default_args, named_default\r\n\r\n\r\n# Note that even non-dynamic default args can be formatted because\r\n# both are saved for populating positional-only defaults args\r\n@dynamic_default_args(format_doc=True)\r\ndef foo(a0,\r\n a1=named_default(a1=5),\r\n a2=3,\r\n /,\r\n a3=named_default(a3=slice(0, 3)),\r\n a4=-1,\r\n *a5,\r\n a6=None,\r\n a7=named_default(a7='python'),\r\n **a8):\r\n \"\"\"\r\n A Foo function that has dynamic default arguments.\r\n\r\n Args:\r\n a0: Required Positional-only argument a0.\r\n a1: Positional-only argument a1. Dynamically defaults to a0={a1}.\r\n a2: Positional-only argument a1. Defaults to {a2}.\r\n a3: Positional-or-keyword argument a2. Dynamically defaults to a3={a3}.\r\n a4: Positional-or-keyword argument a4. Defaults to {a4}\r\n *a5: Varargs a5.\r\n a6: Keyword-only argument a5. Defaults to {a6}.\r\n a7: Keyword-only argument a6. Dynamically defaults to {a7}.\r\n **a8: Varkeywords a8.\r\n \"\"\"\r\n print(f'Called with: a0={a0}, a1={a1}, a2={a2}, a3={a3}, '\r\n f'a4={a4}, a5={a5}, a6={a6}, a7={a7}, a8={a8}')\r\n\r\n\r\n# test output:\r\nfoo(0)\r\n# Called with: a0=0, a1=5, a2=3, a3=slice(0, 3, None), a4=-1, a5=(), a6=None, a7=python, a8={}\r\n```\r\n\r\n#### How it works?\r\n\r\nInternally, the auto generated wrapper with similar signature for this function will be (without formatting):\r\n\r\n```python\r\ndef wrapper(a0, a1=a1_, a2=a2_, a3=a3_, a4=a4_, *a5, a6=a6_, a7=a7_, **a8):\r\n func(a0,\r\n a1._value if isinstance(a1, default) else a1,\r\n a2,\r\n a3._value if isinstance(a3, default) else a3,\r\n a4,\r\n *a5,\r\n a6=a6,\r\n a7=a7._value if isinstance(a7, default) else a7,\r\n **a8)\r\n```\r\n\r\nwhose defaults are set to those of `func`(`=foo`), but the contained `named_default`s will be type checked and have\r\ntheir values forwarded instead.\r\nHow the arguments are forwared depend on the type of arguments:\r\n\r\n- **Positional-only**: with its name, e.g.`a0`, `a1`, `a2`\r\n- **Keyword-or-position**: with its name, e.g. `a3`, `a4`\r\n- **Varargs**: with an asterisk operator, e.g. `*a5`\r\n- **Keyword-only**: with its name as key, e.g. `a6=a6`, `a7=a7`\r\n- **Varkeywords**: with double asterick operator, e.g. `**a8`\r\n\r\n**Note:** _For those who don't know, the type of argument depends on its position relative to the 3 syntax's `/`, `*`,\r\nand `**`:_\r\n\r\n```python\r\ndef f(po0, ___, /, pok0, ____, *args, kw0, kw1, _____, **kwargs):\r\n# --------- ----------- | ---------------- |\r\n# | | | | |\r\n# | Positional - | | Varkeywords\r\n# | or -keyword | Keyword - only\r\n# Positional - only Varargs\r\n ...\r\n```\r\n\r\n**Note:** _The aliases `wrapper, func, default` are assured to be different from the original arguments' names._\r\n\r\n#### Docstring formatting\r\n\r\nBy configuring `@named_default_args(format_doc=True)` (which is the default behavior), the decorator will try to bind\r\nthe default values of arguments with names defined in format keys `{[argument_name]}` in the docstring.\r\nAny modification to the `value` property of `named_default` will update the docstring with an event.\r\n\r\n```python\r\nnamed_default('a1').value *= 2\r\nnamed_default('a3').value = range(10)\r\nnamed_default('a7').value = 'rust'\r\nhelp(foo)\r\n```\r\n\r\nOutput: _(even normal default arguments will be formatted)_\r\n\r\n```\r\nfoo(a0, a1=10, a2=3, /, a3=range(0, 10), a4=-1, *a5, a6=None, a7='rust', **a8)\r\n A Foo function that has dynamic default arguments.\r\n \r\n Args:\r\n a0: Required Positional-only argument a0.\r\n a1: Positional-only argument a1. Dynamically defaults to a0=10.\r\n a2: Positional-only argument a1. Defaults to 3.\r\n a3: Positional-or-keyword argument a2. Dynamically defaults to a3=range(0, 10).\r\n a4: Positional-or-keyword argument a4. Defaults to -1\r\n *a5: Varargs a5.\r\n a6: Keyword-only argument a5. Defaults to None.\r\n a7: Keyword-only argument a6. Dynamically defaults to rust.\r\n **a8: Varkeywords a8.\r\n```\r\n\r\n#### Binding\r\n\r\nThe `named_default` object will emit an event to all registered listeners when its `value` property is modified. \r\nYou can register your own handler by calling `.connect` method:\r\n\r\n```python\r\nfrom dynamic_default_args import named_default\r\n\r\nvariable = named_default('my_variable', None)\r\n\r\n\r\ndef on_variable_changed(value):\r\n print(f'Changed to {value}')\r\n\r\n\r\nvariable.connect(on_variable_changed)\r\n\r\n# modifying the slot has no effect\r\n# but accessing its value is much faster this way\r\nvariable._value = 'this doesn\\'t work'\r\n\r\nvariable.value = 'this works!'\r\n# Changed to this works!\r\n```\r\n\r\n### Limitations\r\n\r\nThis solution relies on function introspection provided by the `inspect` module, which does not work on built-in\r\nfunctions (including C/C++ extensions).\r\nHowever, you can wrap them with your own Python function.\r\n\r\nFor **Cython** users, a `def` or `cpdef` (which might be inspected incorrectly) function defined in `.pyx` files can be\r\ndecorated by setting `binding=True`.\r\n\r\n```cython\r\nimport cython\r\n\r\nfrom dynamic_default_args import dynamic_default_args, named_default\r\n\r\n@dynamic_default_args(format_doc=True)\r\n@cython.binding(True)\r\ndef add(x: float = named_default(x=0.),\r\n y: float = named_default(y=0.)):\r\n \"\"\"``cython.binding`` will add docstring to the wrapped function,\r\n so we can format it later.\r\n\r\n Args:\r\n x: First argument, dynamically defaults to {x}\r\n y: Second argument, dynamically defaults to {y}\r\n\r\n Returns:\r\n The sum of x and y\r\n \"\"\"\r\n return x + y\r\n```\r\n\r\nAlso, it is clear that decorators are not lazily initialized.\r\n\r\n**Further improvements:**\r\n\r\nModifying the `func.__defaults__` should be more performant.\r\n\r\n### License\r\n\r\nThe code is released under MIT-0 license. See [`LICENSE.txt`](LICENSE.txt) for details.\r\nFeel free to do anything, I would be surprised if anyone does use this \ud83d\ude10.\r\n",
"bugtrack_url": null,
"license": "MIT-0",
"summary": "Python dynamic default arguments.",
"version": "0.0.6",
"split_keywords": [
"dynamic-default-args"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "69f45f205663833c5f4e843ce999dbd2b8e8527248e1e153b01766e12a45c4a5",
"md5": "7e7e929c2a8feed63a7f40c0c83aa7a6",
"sha256": "0467c7c752045db6ccfba3b05c94f415c0088d234f05eb67540699c9d01e3154"
},
"downloads": -1,
"filename": "dynamic_default_args-0.0.6-py3-none-any.whl",
"has_sig": false,
"md5_digest": "7e7e929c2a8feed63a7f40c0c83aa7a6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3",
"size": 9372,
"upload_time": "2023-03-30T08:02:06",
"upload_time_iso_8601": "2023-03-30T08:02:06.970554Z",
"url": "https://files.pythonhosted.org/packages/69/f4/5f205663833c5f4e843ce999dbd2b8e8527248e1e153b01766e12a45c4a5/dynamic_default_args-0.0.6-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "89962d3da1a1e04bae25926f98bfc3247737880e46ccbff5a2ac539abcd8f9bf",
"md5": "cf9eed5d364ce9d668fd3d720d2d34c5",
"sha256": "b6f618874d05c62128ce3d1fe0033595f4640d5c62d550e671b3d8e3df9c36c9"
},
"downloads": -1,
"filename": "dynamic-default-args-0.0.6.tar.gz",
"has_sig": false,
"md5_digest": "cf9eed5d364ce9d668fd3d720d2d34c5",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3",
"size": 15961,
"upload_time": "2023-03-30T08:02:08",
"upload_time_iso_8601": "2023-03-30T08:02:08.861674Z",
"url": "https://files.pythonhosted.org/packages/89/96/2d3da1a1e04bae25926f98bfc3247737880e46ccbff5a2ac539abcd8f9bf/dynamic-default-args-0.0.6.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-03-30 08:02:08",
"github": true,
"gitlab": false,
"bitbucket": false,
"github_user": "inspiros",
"github_project": "dynamic-default-args",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "dynamic-default-args"
}