envwrap 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {envwrap-0.2.0 → envwrap-0.3.0}/.github/workflows/test.yml +1 -1
- {envwrap-0.2.0 → envwrap-0.3.0}/.pre-commit-config.yaml +1 -1
- {envwrap-0.2.0 → envwrap-0.3.0}/PKG-INFO +56 -1
- envwrap-0.3.0/README.md +120 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap/__init__.py +25 -11
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap.egg-info/PKG-INFO +56 -1
- envwrap-0.3.0/tests/test_envwrap.py +146 -0
- envwrap-0.2.0/README.md +0 -65
- envwrap-0.2.0/tests/test_envwrap.py +0 -115
- {envwrap-0.2.0 → envwrap-0.3.0}/.github/FUNDING.yml +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/.gitignore +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/LICENCE +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap/cli.py +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap.egg-info/SOURCES.txt +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap.egg-info/dependency_links.txt +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap.egg-info/entry_points.txt +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap.egg-info/requires.txt +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/envwrap.egg-info/top_level.txt +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/pyproject.toml +0 -0
- {envwrap-0.2.0 → envwrap-0.3.0}/setup.cfg +0 -0
|
@@ -69,6 +69,6 @@ jobs:
|
|
|
69
69
|
run: |
|
|
70
70
|
changelog=$(git log --pretty='format:%d%n- %s%n%b---' $(git tag --sort=v:refname | tail -n2 | head -n1)..HEAD)
|
|
71
71
|
tag="${GITHUB_REF#refs/tags/}"
|
|
72
|
-
gh release create --title "
|
|
72
|
+
gh release create --title "envwrap $tag beta" --draft --notes "$changelog" "$tag" dist/${{ steps.dist.outputs.whl }}
|
|
73
73
|
env:
|
|
74
74
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envwrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Override parameter defaults via environment variables & config files
|
|
5
5
|
Maintainer-email: tqdm developers <devs@tqdm.ml>
|
|
6
6
|
License: MPL-2.0
|
|
@@ -39,6 +39,10 @@ Dynamic: license-file
|
|
|
39
39
|
[](https://codecov.io/gh/tqdm/envwrap)
|
|
40
40
|
[](https://app.codacy.com/gh/tqdm/envwrap/dashboard)
|
|
41
41
|
|
|
42
|
+
[](https://github.com/tqdm/envwrap/releases)
|
|
43
|
+
[](https://pypi.org/project/envwrap)
|
|
44
|
+
[](https://anaconda.org/conda-forge/envwrap)
|
|
45
|
+
|
|
42
46
|
Override parameter defaults via environment variables & config files.
|
|
43
47
|
|
|
44
48
|
```py
|
|
@@ -60,10 +64,61 @@ Precedence (descending):
|
|
|
60
64
|
- [platformdirs](https://platformdirs.readthedocs.io/en/latest/parameters.html).{user,site}_config_path(name, False)/
|
|
61
65
|
- `{app}.{toml,yaml,yml,json,ini,cfg}::{func.a,a}`
|
|
62
66
|
- `{name}.{toml,yaml,yml,json,ini,cfg}::{app.func.a,func.a,app.a,a}`
|
|
67
|
+
- ./`pyproject.toml::tool.name.{app.func.a,func.a,app.a,a}`
|
|
63
68
|
- signature (`def foo(a=1)`)
|
|
64
69
|
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Any one of:
|
|
73
|
+
|
|
74
|
+
- `pip install envwrap`
|
|
75
|
+
- `conda install -c conda-forge envwrap`
|
|
76
|
+
- `pip install "git+https://github.com/tqdm/envwrap@main"`
|
|
77
|
+
|
|
78
|
+
> [!TIP]
|
|
79
|
+
> Note that [`tqdm`](https://github.com/tqdm/tqdm) ships with a basic [`tqdm.utils.envwrap`](https://tqdm.github.io/docs/tqdm.utils/#envwrap), which falls back to the original env-var-only (no config file support) implementation if `import envwrap` fails.
|
|
80
|
+
|
|
65
81
|
## Advanced Usage
|
|
66
82
|
|
|
83
|
+
### CLI integration
|
|
84
|
+
|
|
85
|
+
```py
|
|
86
|
+
"""CLI example program using envwrap for configuration management.
|
|
87
|
+
|
|
88
|
+
Usage:
|
|
89
|
+
myapp.py [options] <arg1> [<arg2>]
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
-h, --help Show this help message and exit.
|
|
93
|
+
-o=<value>, --option=<value> An option [default: foo].
|
|
94
|
+
|
|
95
|
+
Arguments:
|
|
96
|
+
<arg1> An argument.
|
|
97
|
+
<arg2> An integer argument [default: 2:int].
|
|
98
|
+
|
|
99
|
+
Defaults above may be overridden by environment variables or config files:
|
|
100
|
+
- `MYAPP[_CLI]_*`
|
|
101
|
+
- `{.,~/.config/myapp,/etc/xdg/myapp}/myapp[/cli].{toml,yml,json,ini}::[cli.]*`
|
|
102
|
+
- `pyproject.toml::tool.myapp[.cli]*`
|
|
103
|
+
"""
|
|
104
|
+
import argopt, shtab, envwrap
|
|
105
|
+
|
|
106
|
+
ONLY_PASS_VALID = True # trim envwrap defaults based on parser's valid actions
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
parser = argopt.argopt(__doc__)
|
|
110
|
+
shtab.add_argument_to(parser)
|
|
111
|
+
|
|
112
|
+
defaults = envwrap.get_defaults("myapp", "", "cli")
|
|
113
|
+
if ONLY_PASS_VALID:
|
|
114
|
+
valid = {i.dest for i in parser._actions}
|
|
115
|
+
defaults = {k: defaults[k] for k in defaults.keys() & valid}
|
|
116
|
+
|
|
117
|
+
parser.set_defaults(**defaults)
|
|
118
|
+
args = parser.parse_args()
|
|
119
|
+
print(args)
|
|
120
|
+
```
|
|
121
|
+
|
|
67
122
|
### Live-reload
|
|
68
123
|
|
|
69
124
|
To force re-reading config files & environment variables without restarting the process:
|
envwrap-0.3.0/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# envwrap
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tqdm/envwrap/actions/workflows/test.yml)
|
|
4
|
+
[](https://coveralls.io/github/tqdm/envwrap)
|
|
5
|
+
[](https://codecov.io/gh/tqdm/envwrap)
|
|
6
|
+
[](https://app.codacy.com/gh/tqdm/envwrap/dashboard)
|
|
7
|
+
|
|
8
|
+
[](https://github.com/tqdm/envwrap/releases)
|
|
9
|
+
[](https://pypi.org/project/envwrap)
|
|
10
|
+
[](https://anaconda.org/conda-forge/envwrap)
|
|
11
|
+
|
|
12
|
+
Override parameter defaults via environment variables & config files.
|
|
13
|
+
|
|
14
|
+
```py
|
|
15
|
+
import envwrap
|
|
16
|
+
|
|
17
|
+
@envwrap.envwrap("name", "app")
|
|
18
|
+
def func(a=1):
|
|
19
|
+
...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Precedence (descending):
|
|
23
|
+
|
|
24
|
+
- call (`func(a=3)`)
|
|
25
|
+
- environment (`NAME_APP_FUNC_A=2`, `NAME_FUNC_A=2`, `NAME_APP_A=2`, `NAME_A=2`)
|
|
26
|
+
- `UPPER_CASE` env vars -> `lower_case` param names
|
|
27
|
+
- other cases aren't supported because Windows ignores case
|
|
28
|
+
- config file:
|
|
29
|
+
- ./`{name}.{toml,yaml,yml,json,ini,cfg}::{app.func.a,func.a,app.a,a}`
|
|
30
|
+
- [platformdirs](https://platformdirs.readthedocs.io/en/latest/parameters.html).{user,site}_config_path(name, False)/
|
|
31
|
+
- `{app}.{toml,yaml,yml,json,ini,cfg}::{func.a,a}`
|
|
32
|
+
- `{name}.{toml,yaml,yml,json,ini,cfg}::{app.func.a,func.a,app.a,a}`
|
|
33
|
+
- ./`pyproject.toml::tool.name.{app.func.a,func.a,app.a,a}`
|
|
34
|
+
- signature (`def foo(a=1)`)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Any one of:
|
|
39
|
+
|
|
40
|
+
- `pip install envwrap`
|
|
41
|
+
- `conda install -c conda-forge envwrap`
|
|
42
|
+
- `pip install "git+https://github.com/tqdm/envwrap@main"`
|
|
43
|
+
|
|
44
|
+
> [!TIP]
|
|
45
|
+
> Note that [`tqdm`](https://github.com/tqdm/tqdm) ships with a basic [`tqdm.utils.envwrap`](https://tqdm.github.io/docs/tqdm.utils/#envwrap), which falls back to the original env-var-only (no config file support) implementation if `import envwrap` fails.
|
|
46
|
+
|
|
47
|
+
## Advanced Usage
|
|
48
|
+
|
|
49
|
+
### CLI integration
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
"""CLI example program using envwrap for configuration management.
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
myapp.py [options] <arg1> [<arg2>]
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
-h, --help Show this help message and exit.
|
|
59
|
+
-o=<value>, --option=<value> An option [default: foo].
|
|
60
|
+
|
|
61
|
+
Arguments:
|
|
62
|
+
<arg1> An argument.
|
|
63
|
+
<arg2> An integer argument [default: 2:int].
|
|
64
|
+
|
|
65
|
+
Defaults above may be overridden by environment variables or config files:
|
|
66
|
+
- `MYAPP[_CLI]_*`
|
|
67
|
+
- `{.,~/.config/myapp,/etc/xdg/myapp}/myapp[/cli].{toml,yml,json,ini}::[cli.]*`
|
|
68
|
+
- `pyproject.toml::tool.myapp[.cli]*`
|
|
69
|
+
"""
|
|
70
|
+
import argopt, shtab, envwrap
|
|
71
|
+
|
|
72
|
+
ONLY_PASS_VALID = True # trim envwrap defaults based on parser's valid actions
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
parser = argopt.argopt(__doc__)
|
|
76
|
+
shtab.add_argument_to(parser)
|
|
77
|
+
|
|
78
|
+
defaults = envwrap.get_defaults("myapp", "", "cli")
|
|
79
|
+
if ONLY_PASS_VALID:
|
|
80
|
+
valid = {i.dest for i in parser._actions}
|
|
81
|
+
defaults = {k: defaults[k] for k in defaults.keys() & valid}
|
|
82
|
+
|
|
83
|
+
parser.set_defaults(**defaults)
|
|
84
|
+
args = parser.parse_args()
|
|
85
|
+
print(args)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Live-reload
|
|
89
|
+
|
|
90
|
+
To force re-reading config files & environment variables without restarting the process:
|
|
91
|
+
|
|
92
|
+
```py
|
|
93
|
+
envwrap.get_defaults.cache_clear()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Debugging
|
|
97
|
+
|
|
98
|
+
A CLI tool can print defaults. For example, with this config:
|
|
99
|
+
|
|
100
|
+
```toml
|
|
101
|
+
# config file: foo.toml
|
|
102
|
+
[test]
|
|
103
|
+
a = 1337
|
|
104
|
+
b = 2
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
python -m envwrap --help
|
|
109
|
+
FOO_A=42 python -m envwrap foo test
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
will print:
|
|
113
|
+
|
|
114
|
+
```py
|
|
115
|
+
>>> @envwrap.envwrap('foo', '')
|
|
116
|
+
>>> def test(...):
|
|
117
|
+
... ...
|
|
118
|
+
will use defaults:
|
|
119
|
+
{'a': '42', 'b': 2, ...}
|
|
120
|
+
```
|
|
@@ -20,26 +20,26 @@ log = logging.getLogger(__name__)
|
|
|
20
20
|
def read_config(fpath: PurePath) -> dict:
|
|
21
21
|
log.debug("Reading: %s", fpath)
|
|
22
22
|
ext = fpath.suffix.lower()[1:]
|
|
23
|
-
if ext ==
|
|
23
|
+
if ext == 'toml':
|
|
24
24
|
try:
|
|
25
25
|
from tomllib import loads # py>=3.11
|
|
26
26
|
except ModuleNotFoundError:
|
|
27
27
|
from toml import loads
|
|
28
|
-
elif ext in (
|
|
28
|
+
elif ext in ('yaml', 'yml'):
|
|
29
29
|
from yaml import safe_load as loads
|
|
30
|
-
elif ext ==
|
|
30
|
+
elif ext == 'json':
|
|
31
31
|
from json import loads
|
|
32
|
-
elif ext in (
|
|
32
|
+
elif ext in ('ini', 'cfg'):
|
|
33
33
|
from configparser import ConfigParser
|
|
34
34
|
parser = ConfigParser()
|
|
35
35
|
parser.read_string(fpath.read_text())
|
|
36
|
-
res = {sec: dict(parser.items(sec)) for sec in parser.sections() if sec.count(
|
|
36
|
+
res = {sec: dict(parser.items(sec)) for sec in parser.sections() if sec.count('.') == 0}
|
|
37
37
|
for sec in parser.sections():
|
|
38
|
-
if sec.count(
|
|
39
|
-
parent, child = sec.split(
|
|
38
|
+
if sec.count('.') == 1:
|
|
39
|
+
parent, child = sec.split('.', 1)
|
|
40
40
|
res.setdefault(parent, {}).setdefault(child, {})
|
|
41
41
|
res[parent][child] |= parser.items(sec)
|
|
42
|
-
elif sec.count(
|
|
42
|
+
elif sec.count('.') > 1:
|
|
43
43
|
warn(f"Skipping nested section: {sec}", UserWarning, stacklevel=2)
|
|
44
44
|
return res
|
|
45
45
|
else:
|
|
@@ -53,6 +53,19 @@ def get_defaults(name: str, app: str, func: str):
|
|
|
53
53
|
"""In-memory (functools.cache) of overrides extracted from config files & env vars."""
|
|
54
54
|
conf = PlatformDirs(name, False)
|
|
55
55
|
overrides = {}
|
|
56
|
+
log.debug("Searching in pyproject.toml::tool.%s", name)
|
|
57
|
+
if (fpath := Path("pyproject.toml")).is_file():
|
|
58
|
+
try:
|
|
59
|
+
cfg = read_config(fpath).get('tool', {}).get(name, {})
|
|
60
|
+
overrides.update(cfg)
|
|
61
|
+
if app in cfg:
|
|
62
|
+
overrides.update(cfg[app])
|
|
63
|
+
if func in cfg:
|
|
64
|
+
overrides.update(cfg[func])
|
|
65
|
+
if app in cfg and func in cfg[app]:
|
|
66
|
+
overrides.update(cfg[app][func])
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
log.debug(f"Exception ignored: {exc}")
|
|
56
69
|
for pth, base in (
|
|
57
70
|
(conf.site_config_path, name),
|
|
58
71
|
(conf.site_config_path, app),
|
|
@@ -107,6 +120,7 @@ def envwrap(name: str, app: str = "", types: dict = None, is_method=False):
|
|
|
107
120
|
- platformdirs.{user,site}_config_path(name, False)/
|
|
108
121
|
- `{app}.{toml,yaml,yml,json,ini,cfg}::{func.a,a}`
|
|
109
122
|
- `{name}.{toml,yaml,yml,json,ini,cfg}::{app.func.a,func.a,app.a,a}`
|
|
123
|
+
- ./`pyproject.toml::tool.name.{app.func.a,func.a,app.a,a}`
|
|
110
124
|
- signature (`def foo(a=1)`)
|
|
111
125
|
|
|
112
126
|
Parameters
|
|
@@ -144,9 +158,9 @@ def envwrap(name: str, app: str = "", types: dict = None, is_method=False):
|
|
|
144
158
|
|
|
145
159
|
def wrap(func):
|
|
146
160
|
params = signature(func).parameters
|
|
147
|
-
|
|
148
|
-
# ignore unknown
|
|
149
|
-
overrides = {k: v for k, v in
|
|
161
|
+
defaults = get_defaults(name, app, func.__name__)
|
|
162
|
+
# ignore unknown params
|
|
163
|
+
overrides = {k: v for k, v in defaults.items() if k in params}
|
|
150
164
|
log.debug("Loaded overrides for %s: %s", func.__name__, overrides)
|
|
151
165
|
# infer overrides' `type`s
|
|
152
166
|
for k in overrides:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envwrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Override parameter defaults via environment variables & config files
|
|
5
5
|
Maintainer-email: tqdm developers <devs@tqdm.ml>
|
|
6
6
|
License: MPL-2.0
|
|
@@ -39,6 +39,10 @@ Dynamic: license-file
|
|
|
39
39
|
[](https://codecov.io/gh/tqdm/envwrap)
|
|
40
40
|
[](https://app.codacy.com/gh/tqdm/envwrap/dashboard)
|
|
41
41
|
|
|
42
|
+
[](https://github.com/tqdm/envwrap/releases)
|
|
43
|
+
[](https://pypi.org/project/envwrap)
|
|
44
|
+
[](https://anaconda.org/conda-forge/envwrap)
|
|
45
|
+
|
|
42
46
|
Override parameter defaults via environment variables & config files.
|
|
43
47
|
|
|
44
48
|
```py
|
|
@@ -60,10 +64,61 @@ Precedence (descending):
|
|
|
60
64
|
- [platformdirs](https://platformdirs.readthedocs.io/en/latest/parameters.html).{user,site}_config_path(name, False)/
|
|
61
65
|
- `{app}.{toml,yaml,yml,json,ini,cfg}::{func.a,a}`
|
|
62
66
|
- `{name}.{toml,yaml,yml,json,ini,cfg}::{app.func.a,func.a,app.a,a}`
|
|
67
|
+
- ./`pyproject.toml::tool.name.{app.func.a,func.a,app.a,a}`
|
|
63
68
|
- signature (`def foo(a=1)`)
|
|
64
69
|
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Any one of:
|
|
73
|
+
|
|
74
|
+
- `pip install envwrap`
|
|
75
|
+
- `conda install -c conda-forge envwrap`
|
|
76
|
+
- `pip install "git+https://github.com/tqdm/envwrap@main"`
|
|
77
|
+
|
|
78
|
+
> [!TIP]
|
|
79
|
+
> Note that [`tqdm`](https://github.com/tqdm/tqdm) ships with a basic [`tqdm.utils.envwrap`](https://tqdm.github.io/docs/tqdm.utils/#envwrap), which falls back to the original env-var-only (no config file support) implementation if `import envwrap` fails.
|
|
80
|
+
|
|
65
81
|
## Advanced Usage
|
|
66
82
|
|
|
83
|
+
### CLI integration
|
|
84
|
+
|
|
85
|
+
```py
|
|
86
|
+
"""CLI example program using envwrap for configuration management.
|
|
87
|
+
|
|
88
|
+
Usage:
|
|
89
|
+
myapp.py [options] <arg1> [<arg2>]
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
-h, --help Show this help message and exit.
|
|
93
|
+
-o=<value>, --option=<value> An option [default: foo].
|
|
94
|
+
|
|
95
|
+
Arguments:
|
|
96
|
+
<arg1> An argument.
|
|
97
|
+
<arg2> An integer argument [default: 2:int].
|
|
98
|
+
|
|
99
|
+
Defaults above may be overridden by environment variables or config files:
|
|
100
|
+
- `MYAPP[_CLI]_*`
|
|
101
|
+
- `{.,~/.config/myapp,/etc/xdg/myapp}/myapp[/cli].{toml,yml,json,ini}::[cli.]*`
|
|
102
|
+
- `pyproject.toml::tool.myapp[.cli]*`
|
|
103
|
+
"""
|
|
104
|
+
import argopt, shtab, envwrap
|
|
105
|
+
|
|
106
|
+
ONLY_PASS_VALID = True # trim envwrap defaults based on parser's valid actions
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
parser = argopt.argopt(__doc__)
|
|
110
|
+
shtab.add_argument_to(parser)
|
|
111
|
+
|
|
112
|
+
defaults = envwrap.get_defaults("myapp", "", "cli")
|
|
113
|
+
if ONLY_PASS_VALID:
|
|
114
|
+
valid = {i.dest for i in parser._actions}
|
|
115
|
+
defaults = {k: defaults[k] for k in defaults.keys() & valid}
|
|
116
|
+
|
|
117
|
+
parser.set_defaults(**defaults)
|
|
118
|
+
args = parser.parse_args()
|
|
119
|
+
print(args)
|
|
120
|
+
```
|
|
121
|
+
|
|
67
122
|
### Live-reload
|
|
68
123
|
|
|
69
124
|
To force re-reading config files & environment variables without restarting the process:
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from sys import version_info
|
|
5
|
+
from textwrap import dedent
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from envwrap import cli, envwrap, get_defaults
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_config(fpath, cfg):
|
|
13
|
+
ext = fpath.suffix.lower()[1:]
|
|
14
|
+
fpath.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
if ext == 'toml':
|
|
16
|
+
from toml import dumps
|
|
17
|
+
elif ext in ('yaml', 'yml'):
|
|
18
|
+
from yaml import safe_dump as dumps
|
|
19
|
+
elif ext == 'json':
|
|
20
|
+
from json import dumps
|
|
21
|
+
elif ext in ('ini', 'cfg'):
|
|
22
|
+
from configparser import ConfigParser
|
|
23
|
+
parser = ConfigParser()
|
|
24
|
+
for sec, items in cfg.items():
|
|
25
|
+
parser[sec] = {k: v for k, v in items.items() if not isinstance(v, dict)}
|
|
26
|
+
for subsec, subitems in items.items():
|
|
27
|
+
if isinstance(subitems, dict):
|
|
28
|
+
parser[f"{sec}.{subsec}"] = subitems
|
|
29
|
+
with fpath.open('w') as fd:
|
|
30
|
+
return parser.write(fd)
|
|
31
|
+
else:
|
|
32
|
+
raise TypeError(f"Unsupported config filetype: {fpath}")
|
|
33
|
+
fpath.write_text(dumps(cfg))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture(autouse=True)
|
|
37
|
+
def set_env():
|
|
38
|
+
os.environ['ENVWRAP_B'] = "42"
|
|
39
|
+
os.environ['ENVWRAP_C'] = "1337"
|
|
40
|
+
os.environ['ENVWRAP_TESTENV_D'] = "360"
|
|
41
|
+
os.environ['ENVWRAP_FUNCNAME_E'] = "101"
|
|
42
|
+
os.environ['ENVWRAP_TESTENV_FUNCNAME_F'] = "404"
|
|
43
|
+
get_defaults.cache_clear()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def funcname(a: int = None, b=2, c=3, d=4, e=5, f=6):
|
|
47
|
+
return {'a': a, 'b': b, 'c': c, 'd': d, 'e': e, 'f': f}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_env():
|
|
51
|
+
wrapped = envwrap('envwrap', 'testenv')(funcname)
|
|
52
|
+
assert wrapped(c=99) == {'a': None, 'b': 42, 'c': 99, 'd': 360, 'e': 101, 'f': 404}
|
|
53
|
+
wrapped = envwrap('envwrap')(funcname)
|
|
54
|
+
assert wrapped(c=99) == {'a': None, 'b': 42, 'c': 99, 'd': 4, 'e': 101, 'f': 6}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize('ext', ['toml', 'yaml', 'yml', 'json', 'ini', 'cfg'])
|
|
58
|
+
@pytest.mark.parametrize('base', ['cfgwrap', 'testcfg'])
|
|
59
|
+
def test_conf(tmp_path, base, ext):
|
|
60
|
+
if version_info < (3, 9) and ext in ('ini', 'cfg'):
|
|
61
|
+
pytest.skip("configparser dict merging requires python>=3.9")
|
|
62
|
+
config = {
|
|
63
|
+
'testcfg': {'b': 43, 'c': 1338, 'd': 361,
|
|
64
|
+
'funcname': {'f': 405}}, 'funcname': {'e': 102, 'a': 0},
|
|
65
|
+
'cfgwrap': {'b': -1, 'e': -2, 'f': -3, 'funcname': {'e': -4}}}
|
|
66
|
+
write_config(tmp_path / f"{base}.{ext}", config)
|
|
67
|
+
pwd = os.curdir
|
|
68
|
+
os.chdir(tmp_path)
|
|
69
|
+
try:
|
|
70
|
+
wrapped = envwrap('cfgwrap', 'testcfg')(funcname)
|
|
71
|
+
if base == 'cfgwrap':
|
|
72
|
+
assert wrapped(c=98) == {'a': 0, 'b': 43, 'c': 98, 'd': 361, 'e': 102, 'f': 405}
|
|
73
|
+
else:
|
|
74
|
+
assert wrapped(c=98) == {'a': None, 'b': 2, 'c': 98, 'd': 4, 'e': 5, 'f': 6}
|
|
75
|
+
assert int(get_defaults(base, 'testcfg', 'funcname')['a']) == 0
|
|
76
|
+
assert int(get_defaults(base, 'testcfg', 'funcname')['f']) == 405
|
|
77
|
+
assert int(get_defaults(base, 'testcfg', 'miss-n/a')['d']) == 361
|
|
78
|
+
assert int(get_defaults(base, 'cfgwrap', 'funcname')['b']) == -1
|
|
79
|
+
assert int(get_defaults(base, 'cfgwrap', 'miss-n/a')['e']) == -2
|
|
80
|
+
assert int(get_defaults(base, 'cfgwrap', 'funcname')['f']) == -3
|
|
81
|
+
assert int(get_defaults(base, 'cfgwrap', 'funcname')['e']) == -4
|
|
82
|
+
finally:
|
|
83
|
+
os.chdir(pwd)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_pyproject(tmp_path):
|
|
87
|
+
pwd = os.curdir
|
|
88
|
+
os.chdir(tmp_path)
|
|
89
|
+
try:
|
|
90
|
+
shutil.copy(Path(__file__).parent.parent / "pyproject.toml", "pyproject.toml")
|
|
91
|
+
for tool, key in (('isort', 'line_length'), ('flake8', 'max_line_length'),
|
|
92
|
+
('yapf', 'column_limit')):
|
|
93
|
+
assert get_defaults(tool, '', '')[key] == 99
|
|
94
|
+
|
|
95
|
+
assert get_defaults('coverage', '', 'report')['show_missing'] is True
|
|
96
|
+
assert get_defaults('coverage', 'report', '')['show_missing'] is True
|
|
97
|
+
assert get_defaults('coverage', 'report', 'show_missing')['report']['show_missing'] is True
|
|
98
|
+
finally:
|
|
99
|
+
os.chdir(pwd)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_env_cli(capsys):
|
|
103
|
+
cli.main(['envwrap', 'testenv', 'funcname'])
|
|
104
|
+
out, err = capsys.readouterr()
|
|
105
|
+
assert out == dedent("""\
|
|
106
|
+
>>> @envwrap.envwrap('envwrap', 'testenv')
|
|
107
|
+
>>> def funcname(...):
|
|
108
|
+
... ...
|
|
109
|
+
will use defaults:
|
|
110
|
+
{'b': '42',
|
|
111
|
+
'c': '1337',
|
|
112
|
+
'd': '360',
|
|
113
|
+
'e': '101',
|
|
114
|
+
'f': '404',
|
|
115
|
+
'funcname_e': '101',
|
|
116
|
+
'funcname_f': '404',
|
|
117
|
+
'testenv_d': '360',
|
|
118
|
+
'testenv_funcname_f': '404'}
|
|
119
|
+
""")
|
|
120
|
+
assert not err
|
|
121
|
+
|
|
122
|
+
cli.main(['envwrap', 'funcname'])
|
|
123
|
+
out, err = capsys.readouterr()
|
|
124
|
+
assert out == dedent("""\
|
|
125
|
+
>>> @envwrap.envwrap('envwrap', '')
|
|
126
|
+
>>> def funcname(...):
|
|
127
|
+
... ...
|
|
128
|
+
will use defaults:
|
|
129
|
+
{'b': '42',
|
|
130
|
+
'c': '1337',
|
|
131
|
+
'e': '101',
|
|
132
|
+
'funcname_e': '101',
|
|
133
|
+
'testenv_d': '360',
|
|
134
|
+
'testenv_funcname_f': '404'}
|
|
135
|
+
""")
|
|
136
|
+
assert not err
|
|
137
|
+
|
|
138
|
+
with pytest.raises(ValueError):
|
|
139
|
+
cli.main(['envwrap'])
|
|
140
|
+
with pytest.raises(ValueError):
|
|
141
|
+
cli.main(['envwrap', 'testenv', 'funcname', 'extra'])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_deprecated_underscore():
|
|
145
|
+
with pytest.warns(DeprecationWarning, match="Trailing underscore"):
|
|
146
|
+
envwrap('envwrap_', 'testenv')(funcname)
|
envwrap-0.2.0/README.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# envwrap
|
|
2
|
-
|
|
3
|
-
[](https://github.com/tqdm/envwrap/actions/workflows/test.yml)
|
|
4
|
-
[](https://coveralls.io/github/tqdm/envwrap)
|
|
5
|
-
[](https://codecov.io/gh/tqdm/envwrap)
|
|
6
|
-
[](https://app.codacy.com/gh/tqdm/envwrap/dashboard)
|
|
7
|
-
|
|
8
|
-
Override parameter defaults via environment variables & config files.
|
|
9
|
-
|
|
10
|
-
```py
|
|
11
|
-
import envwrap
|
|
12
|
-
|
|
13
|
-
@envwrap.envwrap("name", "app")
|
|
14
|
-
def func(a=1):
|
|
15
|
-
...
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Precedence (descending):
|
|
19
|
-
|
|
20
|
-
- call (`func(a=3)`)
|
|
21
|
-
- environment (`NAME_APP_FUNC_A=2`, `NAME_FUNC_A=2`, `NAME_APP_A=2`, `NAME_A=2`)
|
|
22
|
-
- `UPPER_CASE` env vars -> `lower_case` param names
|
|
23
|
-
- other cases aren't supported because Windows ignores case
|
|
24
|
-
- config file:
|
|
25
|
-
- ./`{name}.{toml,yaml,yml,json,ini,cfg}::{app.func.a,func.a,app.a,a}`
|
|
26
|
-
- [platformdirs](https://platformdirs.readthedocs.io/en/latest/parameters.html).{user,site}_config_path(name, False)/
|
|
27
|
-
- `{app}.{toml,yaml,yml,json,ini,cfg}::{func.a,a}`
|
|
28
|
-
- `{name}.{toml,yaml,yml,json,ini,cfg}::{app.func.a,func.a,app.a,a}`
|
|
29
|
-
- signature (`def foo(a=1)`)
|
|
30
|
-
|
|
31
|
-
## Advanced Usage
|
|
32
|
-
|
|
33
|
-
### Live-reload
|
|
34
|
-
|
|
35
|
-
To force re-reading config files & environment variables without restarting the process:
|
|
36
|
-
|
|
37
|
-
```py
|
|
38
|
-
envwrap.get_defaults.cache_clear()
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Debugging
|
|
42
|
-
|
|
43
|
-
A CLI tool can print defaults. For example, with this config:
|
|
44
|
-
|
|
45
|
-
```toml
|
|
46
|
-
# config file: foo.toml
|
|
47
|
-
[test]
|
|
48
|
-
a = 1337
|
|
49
|
-
b = 2
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
```sh
|
|
53
|
-
python -m envwrap --help
|
|
54
|
-
FOO_A=42 python -m envwrap foo test
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
will print:
|
|
58
|
-
|
|
59
|
-
```py
|
|
60
|
-
>>> @envwrap.envwrap('foo', '')
|
|
61
|
-
>>> def test(...):
|
|
62
|
-
... ...
|
|
63
|
-
will use defaults:
|
|
64
|
-
{'a': '42', 'b': 2, ...}
|
|
65
|
-
```
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from sys import version_info
|
|
3
|
-
from textwrap import dedent
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from envwrap import cli, envwrap, get_defaults
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def write_config(fpath, cfg):
|
|
11
|
-
ext = fpath.suffix.lower()[1:]
|
|
12
|
-
fpath.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
-
if ext == "toml":
|
|
14
|
-
from toml import dumps
|
|
15
|
-
elif ext in ("yaml", "yml"):
|
|
16
|
-
from yaml import safe_dump as dumps
|
|
17
|
-
elif ext == "json":
|
|
18
|
-
from json import dumps
|
|
19
|
-
elif ext in ("ini", "cfg"):
|
|
20
|
-
from configparser import ConfigParser
|
|
21
|
-
parser = ConfigParser()
|
|
22
|
-
for sec, items in cfg.items():
|
|
23
|
-
parser[sec] = {k: v for k, v in items.items() if not isinstance(v, dict)}
|
|
24
|
-
for subsec, subitems in items.items():
|
|
25
|
-
if isinstance(subitems, dict):
|
|
26
|
-
parser[f"{sec}.{subsec}"] = subitems
|
|
27
|
-
with fpath.open("w") as fd:
|
|
28
|
-
return parser.write(fd)
|
|
29
|
-
else:
|
|
30
|
-
raise TypeError(f"Unsupported config filetype: {fpath}")
|
|
31
|
-
fpath.write_text(dumps(cfg))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@pytest.fixture(autouse=True)
|
|
35
|
-
def set_env():
|
|
36
|
-
os.environ["ENVWRAP_B"] = "42"
|
|
37
|
-
os.environ["ENVWRAP_C"] = "1337"
|
|
38
|
-
os.environ["ENVWRAP_TESTENV_D"] = "360"
|
|
39
|
-
os.environ["ENVWRAP_FUNCNAME_E"] = "101"
|
|
40
|
-
os.environ["ENVWRAP_TESTENV_FUNCNAME_F"] = "404"
|
|
41
|
-
get_defaults.cache_clear()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def funcname(a=1, b=2, c=3, d=4, e=5, f=6):
|
|
45
|
-
return {"a": a, "b": b, "c": c, "d": d, "e": e, "f": f}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_env():
|
|
49
|
-
f = envwrap("envwrap", "testenv")(funcname)
|
|
50
|
-
assert f(c=99) == {"a": 1, "b": 42, "c": 99, "d": 360, "e": 101, "f": 404}
|
|
51
|
-
f = envwrap("envwrap")(funcname)
|
|
52
|
-
assert f(c=99) == {"a": 1, "b": 42, "c": 99, "d": 4, "e": 101, "f": 6}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@pytest.mark.parametrize("ext", ["toml", "yaml", "yml", "json", "ini", "cfg"])
|
|
56
|
-
def test_conf(tmp_path, ext):
|
|
57
|
-
if version_info < (3, 9) and ext in ("ini", "cfg"):
|
|
58
|
-
pytest.skip("configparser dict merging requires python>=3.9")
|
|
59
|
-
config = {
|
|
60
|
-
"testcfg": {"b": 43, "c": 1338, "d": 361, "funcname": {"f": 405}}, "funcname": {"e": 102}}
|
|
61
|
-
write_config(tmp_path / f"cfgwrap.{ext}", config)
|
|
62
|
-
pwd = os.curdir
|
|
63
|
-
os.chdir(tmp_path)
|
|
64
|
-
try:
|
|
65
|
-
f = envwrap("cfgwrap", "testcfg")(funcname)
|
|
66
|
-
assert f(c=98) == {"a": 1, "b": 43, "c": 98, "d": 361, "e": 102, "f": 405}
|
|
67
|
-
finally:
|
|
68
|
-
os.chdir(pwd)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_env_cli(capsys):
|
|
72
|
-
cli.main(["envwrap", "testenv", "funcname"])
|
|
73
|
-
out, err = capsys.readouterr()
|
|
74
|
-
assert out == dedent("""\
|
|
75
|
-
>>> @envwrap.envwrap('envwrap', 'testenv')
|
|
76
|
-
>>> def funcname(...):
|
|
77
|
-
... ...
|
|
78
|
-
will use defaults:
|
|
79
|
-
{'b': '42',
|
|
80
|
-
'c': '1337',
|
|
81
|
-
'd': '360',
|
|
82
|
-
'e': '101',
|
|
83
|
-
'f': '404',
|
|
84
|
-
'funcname_e': '101',
|
|
85
|
-
'funcname_f': '404',
|
|
86
|
-
'testenv_d': '360',
|
|
87
|
-
'testenv_funcname_f': '404'}
|
|
88
|
-
""")
|
|
89
|
-
assert not err
|
|
90
|
-
|
|
91
|
-
cli.main(["envwrap", "funcname"])
|
|
92
|
-
out, err = capsys.readouterr()
|
|
93
|
-
assert out == dedent("""\
|
|
94
|
-
>>> @envwrap.envwrap('envwrap', '')
|
|
95
|
-
>>> def funcname(...):
|
|
96
|
-
... ...
|
|
97
|
-
will use defaults:
|
|
98
|
-
{'b': '42',
|
|
99
|
-
'c': '1337',
|
|
100
|
-
'e': '101',
|
|
101
|
-
'funcname_e': '101',
|
|
102
|
-
'testenv_d': '360',
|
|
103
|
-
'testenv_funcname_f': '404'}
|
|
104
|
-
""")
|
|
105
|
-
assert not err
|
|
106
|
-
|
|
107
|
-
with pytest.raises(ValueError):
|
|
108
|
-
cli.main(["envwrap"])
|
|
109
|
-
with pytest.raises(ValueError):
|
|
110
|
-
cli.main(["envwrap", "testenv", "funcname", "extra"])
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def test_deprecated_underscore():
|
|
114
|
-
with pytest.warns(DeprecationWarning, match="Trailing underscore"):
|
|
115
|
-
envwrap("envwrap_", "testenv")(funcname)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|