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.
@@ -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 "tqdm $tag stable" --draft --notes "$changelog" "$tag" dist/${{ steps.dist.outputs.whl }} dist/${{ steps.dist.outputs.whl_asc }}
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 }}
@@ -30,7 +30,7 @@ repos:
30
30
  name: pytest
31
31
  language: python
32
32
  entry: env PYTHONPATH=. pytest
33
- args: [--cov=envwrap]
33
+ args: [--cov=envwrap, -vv]
34
34
  types: [python]
35
35
  pass_filenames: false
36
36
  additional_dependencies:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: envwrap
3
- Version: 0.2.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
  [![codecov](https://codecov.io/gh/tqdm/envwrap/graph/badge.svg?token=PEWICBIPVW)](https://codecov.io/gh/tqdm/envwrap)
40
40
  [![codacy](https://app.codacy.com/project/badge/Grade/6ca7a441560444489fd5c5b1548ab0de)](https://app.codacy.com/gh/tqdm/envwrap/dashboard)
41
41
 
42
+ [![releases](https://img.shields.io/pypi/v/envwrap.svg?label=changelog)](https://github.com/tqdm/envwrap/releases)
43
+ [![pypi/envwrap](https://img.shields.io/pypi/pyversions/envwrap.svg?logo=python&logoColor=white)](https://pypi.org/project/envwrap)
44
+ [![conda-forge::envwrap](https://img.shields.io/conda/v/conda-forge/envwrap.svg?label=conda-forge&logo=conda-forge)](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,120 @@
1
+ # envwrap
2
+
3
+ [![CI](https://github.com/tqdm/envwrap/actions/workflows/test.yml/badge.svg)](https://github.com/tqdm/envwrap/actions/workflows/test.yml)
4
+ [![coveralls](https://img.shields.io/coveralls/github/tqdm/envwrap/main?logo=coveralls)](https://coveralls.io/github/tqdm/envwrap)
5
+ [![codecov](https://codecov.io/gh/tqdm/envwrap/graph/badge.svg?token=PEWICBIPVW)](https://codecov.io/gh/tqdm/envwrap)
6
+ [![codacy](https://app.codacy.com/project/badge/Grade/6ca7a441560444489fd5c5b1548ab0de)](https://app.codacy.com/gh/tqdm/envwrap/dashboard)
7
+
8
+ [![releases](https://img.shields.io/pypi/v/envwrap.svg?label=changelog)](https://github.com/tqdm/envwrap/releases)
9
+ [![pypi/envwrap](https://img.shields.io/pypi/pyversions/envwrap.svg?logo=python&logoColor=white)](https://pypi.org/project/envwrap)
10
+ [![conda-forge::envwrap](https://img.shields.io/conda/v/conda-forge/envwrap.svg?label=conda-forge&logo=conda-forge)](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 == "toml":
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 ("yaml", "yml"):
28
+ elif ext in ('yaml', 'yml'):
29
29
  from yaml import safe_load as loads
30
- elif ext == "json":
30
+ elif ext == 'json':
31
31
  from json import loads
32
- elif ext in ("ini", "cfg"):
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(".") == 0}
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(".") == 1:
39
- parent, child = sec.split(".", 1)
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(".") > 1:
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
- env_overrides = get_defaults(name, app, func.__name__)
148
- # ignore unknown env vars
149
- overrides = {k: v for k, v in env_overrides.items() if k in params}
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.2.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
  [![codecov](https://codecov.io/gh/tqdm/envwrap/graph/badge.svg?token=PEWICBIPVW)](https://codecov.io/gh/tqdm/envwrap)
40
40
  [![codacy](https://app.codacy.com/project/badge/Grade/6ca7a441560444489fd5c5b1548ab0de)](https://app.codacy.com/gh/tqdm/envwrap/dashboard)
41
41
 
42
+ [![releases](https://img.shields.io/pypi/v/envwrap.svg?label=changelog)](https://github.com/tqdm/envwrap/releases)
43
+ [![pypi/envwrap](https://img.shields.io/pypi/pyversions/envwrap.svg?logo=python&logoColor=white)](https://pypi.org/project/envwrap)
44
+ [![conda-forge::envwrap](https://img.shields.io/conda/v/conda-forge/envwrap.svg?label=conda-forge&logo=conda-forge)](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
- [![CI](https://github.com/tqdm/envwrap/actions/workflows/test.yml/badge.svg)](https://github.com/tqdm/envwrap/actions/workflows/test.yml)
4
- [![coveralls](https://img.shields.io/coveralls/github/tqdm/envwrap/main?logo=coveralls)](https://coveralls.io/github/tqdm/envwrap)
5
- [![codecov](https://codecov.io/gh/tqdm/envwrap/graph/badge.svg?token=PEWICBIPVW)](https://codecov.io/gh/tqdm/envwrap)
6
- [![codacy](https://app.codacy.com/project/badge/Grade/6ca7a441560444489fd5c5b1548ab0de)](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