Skip to content

Commit 6a15a30

Browse files
author
Denis Savran
committed
Add ability to pass prefix
Add 'prefix' keyword argument to 'environ.to_config()', 'environ.generate_help()' and related class methods.
1 parent 8b207df commit 6a15a30

6 files changed

+172
-19
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Whenever there is a need to break compatibility, it is announced here in the cha
1818

1919
## [Unreleased](https://github.com/hynek/environ-config/compare/24.1.0...HEAD)
2020

21+
### Changed
22+
23+
- Add `prefix` keyword argument to `environ.to_config()`, `environ.generate_help()` and related class methods.
24+
[#89](https://github.com/hynek/environ-config/pull/89)
2125

2226
## [24.1.0](https://github.com/hynek/environ-config/compare/23.2.0...24.1.0) - 2024-08-08
2327

src/environ/_environ_config.py

+63-14
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,19 @@ def config(
118118
"""
119119

120120
def wrap(cls):
121-
def from_environ_fnc(cls, environ=os.environ):
122-
return __to_config(cls, environ)
123-
124-
def generate_help_fnc(cls, **kwargs):
125-
return __generate_help(cls, **kwargs)
121+
def from_environ_fnc(
122+
cls, environ=os.environ, *, prefix=PREFIX_NOT_SET
123+
):
124+
return __to_config(cls, environ, prefix=prefix)
125+
126+
def generate_help_fnc(
127+
cls,
128+
formatter=None,
129+
*,
130+
prefix=PREFIX_NOT_SET,
131+
**kwargs,
132+
):
133+
return __generate_help(cls, formatter, prefix=prefix, **kwargs)
126134

127135
cls._prefix = prefix
128136
if from_environ is not None:
@@ -168,7 +176,7 @@ def var(
168176
169177
converter:
170178
A callable that is run with the found value and its return value is
171-
used. Please not that it is also run for default values.
179+
used. Please note that it is also run for default values.
172180
173181
validator:
174182
A callable that is run with the final value. See *attrs*'s `chapter
@@ -256,12 +264,14 @@ class Sub:
256264
)
257265

258266

259-
def _default_getter(environ, metadata, prefix, name):
267+
def _default_getter(environ, metadata, prefixes, name):
260268
"""
261269
This default lookup implementation simply gets values from *environ*.
262270
"""
263271
ce = metadata[CNF_KEY]
264-
var = ce.name if ce.name is not None else "_".join((*prefix, name)).upper()
272+
var = (
273+
ce.name if ce.name is not None else "_".join((*prefixes, name)).upper()
274+
)
265275
log.debug("looking for env var '%s'.", var)
266276
try:
267277
return environ[var]
@@ -328,24 +338,46 @@ def _to_config_recurse(config_cls, environ, prefixes, default=RAISE):
328338
return config_cls(**defaulted)
329339

330340

331-
def to_config(config_cls: type[T], environ: dict[str, str] = os.environ) -> T:
341+
def to_config(
342+
config_cls: type[T],
343+
environ: dict[str, str] = os.environ,
344+
*,
345+
prefix: str | Sentinel = PREFIX_NOT_SET,
346+
) -> T:
332347
"""
333348
Load the configuration as declared by *config_cls* from *environ*.
334349
335350
Args:
336351
config_cls: The configuration class to fill.
337352
338-
environ: Source of the configuration. `os.environ` by default.
353+
environ: Source of the configuration. `os.environ` by default.
354+
355+
prefix:
356+
Prefix that is used for environment variables. May be used to
357+
dynamically change a prefix or to instantiate a nested
358+
configuration class without all of its parent configuration
359+
classes.
339360
340361
Returns:
341362
An instance of *config_cls*.
342363
364+
Examples:
365+
366+
.. code-block:: python
367+
368+
environ.to_config(ComponentConfig, prefix="APP_COMPONENT")
369+
343370
This is equivalent to calling ``config_cls.from_environ()``.
371+
372+
.. versionadded:: 24.2.0 *prefix*
344373
"""
345374
# The canonical app prefix might be falsey in which case we'll still set
346375
# the default prefix for this top level config object
347-
app_prefix = tuple(p for p in (_get_prefix(config_cls),) if p)
348-
return _to_config_recurse(config_cls, environ, app_prefix)
376+
if prefix is PREFIX_NOT_SET:
377+
prefix = _get_prefix(config_cls)
378+
379+
prefixes = (prefix,) if prefix else ()
380+
return _to_config_recurse(config_cls, environ, prefixes)
349381

350382

351383
def _format_help_dicts(help_dicts, display_defaults=False):
@@ -457,7 +489,11 @@ def _generate_help_dicts(config_cls, _prefix=PREFIX_NOT_SET):
457489

458490

459491
def generate_help(
460-
config_cls: type[T], formatter: Callable | None = None, **kwargs: Any
492+
config_cls: type[T],
493+
formatter: Callable | None = None,
494+
*,
495+
prefix: str | Sentinel = PREFIX_NOT_SET,
496+
**kwargs: Any,
461497
) -> str:
462498
"""
463499
Autogenerate a help string for a config class.
@@ -472,16 +508,29 @@ def generate_help(
472508
When using the default formatter, passing `True` for
473509
*display_defaults* makes the default values part of the output.
474510
511+
prefix:
512+
Prefix that is used for environment variables. May be used to
513+
dynamically change a prefix or to generate a help string for a
514+
nested configuration class without all of its parent configuration
515+
classes.
516+
475517
Returns:
476518
A help string that can be printed to the user.
477519
520+
Examples:
521+
522+
.. code-block:: python
523+
524+
environ.generate_help(ComponentConfig, prefix="APP_COMPONENT")
525+
478526
This is equivalent to calling ``config_cls.generate_help()``.
479527
480528
.. versionadded:: 19.1.0
529+
.. versionadded:: 24.2.0 *prefix*
481530
"""
482531
if formatter is None:
483532
formatter = _format_help_dicts
484-
help_dicts = _generate_help_dicts(config_cls)
533+
help_dicts = _generate_help_dicts(config_cls, prefix)
485534

486535
return formatter(help_dicts, **kwargs)
487536

tests/test_class_generate_help.py

+12
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,15 @@ def test_generated_helps_equals_display_defaults(display_defaults):
7676
assert environ.generate_help(
7777
ConfigRenamed, display_defaults=display_defaults
7878
) == ConfigRenamed.gen_help(display_defaults=display_defaults)
79+
80+
81+
def test_generate_help_with_prefix():
82+
"""
83+
Class methods generate help using a passed prefix.
84+
"""
85+
assert environ.generate_help(
86+
AppConfig, prefix="FOO"
87+
) == AppConfig.generate_help(prefix="FOO")
88+
assert environ.generate_help(
89+
ConfigRenamed, prefix="BAR"
90+
) == ConfigRenamed.gen_help(prefix="BAR")

tests/test_class_to_config.py

+27
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,30 @@ class FactoryConfig:
9999

100100
assert cfg.x == []
101101
assert cfg.y == "baz"
102+
103+
104+
def test_from_environ_with_prefix():
105+
"""
106+
Class methods create a config using a passed prefix.
107+
"""
108+
foo_environ = {
109+
"APP_HOST": "nope",
110+
"APP_PORT": "0",
111+
"FOO_HOST": "foo",
112+
"FOO_PORT": "1",
113+
}
114+
115+
assert environ.to_config(
116+
AppConfig, foo_environ, prefix="FOO"
117+
) == AppConfig.from_environ(foo_environ, prefix="FOO")
118+
119+
bar_environ = {
120+
"APP_HOST": "nope",
121+
"APP_PORT": "0",
122+
"BAR_HOST": "bar",
123+
"BAR_PORT": "2",
124+
}
125+
126+
assert environ.to_config(
127+
ConfigRenamed, bar_environ, prefix="BAR"
128+
) == ConfigRenamed.from_env(bar_environ, prefix="BAR")

tests/test_environ_config.py

+65-4
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ def test_nested(self):
106106

107107
assert Nested(x="foo", sub=Nested.Sub(y="bar")) == cfg
108108

109+
def test_nested_with_prefix(self):
110+
"""
111+
A config is created using a passed prefix.
112+
"""
113+
env = {"XYZ_X": "nope", "TMP_X": "foo", "TMP_SUB_Y": "bar"}
114+
cfg = environ.to_config(Nested, env, prefix="TMP")
115+
116+
assert Nested(x="foo", sub=Nested.Sub(y="bar")) == cfg
117+
118+
def test_nested_with_prefix_only_child_config(self):
119+
"""
120+
A config is created only for a child config class using a passed
121+
prefix.
122+
"""
123+
env = {"XYZ_X": "nope", "XYZ_SUB_Y": "bar"}
124+
cfg = environ.to_config(Nested.Sub, env, prefix="XYZ_SUB")
125+
126+
assert Nested.Sub(y="bar") == cfg
127+
109128
def test_missing(self):
110129
"""
111130
If a var is missing, a human-readable MissingEnvValueError is raised.
@@ -235,11 +254,11 @@ class Cfg:
235254

236255
assert Cfg("e", 42) == cfg
237256

238-
def test_generate_help_str(self):
257+
def test_generate_help(self):
239258
"""
240259
A help string is generated for a config class.
241260
242-
Presence of defaults are indicated but they are not shown.
261+
Presence of defaults is indicated, but they are not shown.
243262
"""
244263
help_str = environ.generate_help(Parent)
245264
assert (
@@ -260,7 +279,7 @@ def test_generate_help_str(self):
260279
FOO_CHILD_VAR14 (Required)"""
261280
)
262281

263-
def test_generate_help_str_with_defaults(self):
282+
def test_generate_help_with_defaults(self):
264283
"""
265284
A help string is generated for a config class.
266285
@@ -286,7 +305,49 @@ def test_generate_help_str_with_defaults(self):
286305
FOO_CHILD_VAR14 (Required)"""
287306
)
288307

289-
def test_generate_help_str_when_prefix_is_empty(self):
308+
def test_generate_help_with_prefix(self):
309+
"""
310+
A help string is generated for a config class using a passed
311+
prefix.
312+
"""
313+
help_str = environ.generate_help(Parent, prefix="BAR")
314+
assert (
315+
help_str
316+
== """BAR_VAR1 (Required): var1, no default
317+
BAR_VAR2 (Optional): var2, has default
318+
BAR_VAR3 (Required): var3, bool_var, no default
319+
BAR_VAR4 (Optional): var4, bool_var, has default
320+
DOG (Optional): var5, named, has default
321+
CAT (Required): var6, named, no default
322+
BAR_CHILD_VAR7 (Required): var7, no default
323+
BAR_CHILD_VAR8 (Optional): var8, has default
324+
BAR_CHILD_VAR9 (Required): var9, bool_var, no default
325+
BAR_CHILD_VAR10 (Optional): var10, bool_var, has default
326+
DOG2 (Optional): var11, named, has default
327+
CAT2 (Required): var12, named, no default
328+
BAR_CHILD_VAR13 (Optional)
329+
BAR_CHILD_VAR14 (Required)"""
330+
)
331+
332+
def test_generate_help_with_prefix_only_child_config(self):
333+
"""
334+
A help string is generated only for a child config class using a
335+
passed prefix.
336+
"""
337+
help_str = environ.generate_help(Parent.Child, prefix="FOO_CHILD")
338+
assert (
339+
help_str
340+
== """FOO_CHILD_VAR7 (Required): var7, no default
341+
FOO_CHILD_VAR8 (Optional): var8, has default
342+
FOO_CHILD_VAR9 (Required): var9, bool_var, no default
343+
FOO_CHILD_VAR10 (Optional): var10, bool_var, has default
344+
DOG2 (Optional): var11, named, has default
345+
CAT2 (Required): var12, named, no default
346+
FOO_CHILD_VAR13 (Optional)
347+
FOO_CHILD_VAR14 (Required)"""
348+
)
349+
350+
def test_generate_help_no_prefix(self):
290351
"""
291352
Environment variables' names don't start with an underscore
292353
"""

tests/typing/api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Sub:
5454
a_secret: str = aws_secrets.secret()
5555

5656

57-
h: str = environ.generate_help(Config)
57+
h: str = environ.generate_help(Config, prefix="APP")
5858

5959
cfg = environ.to_config(Config, {"APP_X": "123"})
6060

0 commit comments

Comments
 (0)