envbool 0.4.2__tar.gz → 0.6.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.
- {envbool-0.4.2 → envbool-0.6.0}/PKG-INFO +90 -54
- {envbool-0.4.2 → envbool-0.6.0}/README.md +89 -53
- {envbool-0.4.2 → envbool-0.6.0}/pyproject.toml +1 -1
- {envbool-0.4.2 → envbool-0.6.0}/pyproject.toml.orig +1 -1
- envbool-0.6.0/src/envbool/__init__.py +43 -0
- {envbool-0.4.2 → envbool-0.6.0}/src/envbool/_cli.py +7 -19
- {envbool-0.4.2 → envbool-0.6.0}/src/envbool/_core.py +79 -51
- {envbool-0.4.2 → envbool-0.6.0}/src/envbool/_env.py +11 -9
- {envbool-0.4.2 → envbool-0.6.0}/src/envbool/exceptions.py +23 -0
- envbool-0.4.2/src/envbool/__init__.py +0 -56
- envbool-0.4.2/src/envbool/_defaults.py +0 -189
- {envbool-0.4.2 → envbool-0.6.0}/LICENSE +0 -0
- {envbool-0.4.2 → envbool-0.6.0}/src/envbool/__main__.py +0 -0
- {envbool-0.4.2 → envbool-0.6.0}/src/envbool/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envbool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A small Python library and CLI tool for coercing environment variables (and arbitrary strings) into boolean values.
|
|
5
5
|
Keywords: environment variables,boolean,configuration,env,coerce
|
|
6
6
|
Author: Kyle O'Malley
|
|
@@ -33,6 +33,7 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
**Coerce environment variables and strings into booleans — sensibly.**
|
|
34
34
|
|
|
35
35
|
[](https://pypi.org/project/envbool/)
|
|
36
|
+
[](https://snapcraft.io/envbool)
|
|
36
37
|
[](https://pypi.org/project/envbool/)
|
|
37
38
|
[](LICENSE)
|
|
38
39
|
[](https://github.com/jkomalley/envbool/actions/workflows/ci.yml)
|
|
@@ -67,8 +68,6 @@ CACHE = envbool("CACHE")
|
|
|
67
68
|
- **Always returns `bool`.** No `None`, no surprises in your type signatures.
|
|
68
69
|
- **Customizable value sets.** Replace or extend the truthy/falsy words your
|
|
69
70
|
environment uses.
|
|
70
|
-
- **Process-level defaults.** Call `set_defaults()` once at startup instead
|
|
71
|
-
of threading options through every call site.
|
|
72
71
|
- **A CLI for shell scripts.** Exit codes map to truthiness, so it drops
|
|
73
72
|
straight into `&&` / `||` chains.
|
|
74
73
|
- **Zero ceremony.** Zero dependencies, fully typed, Python 3.11+.
|
|
@@ -91,6 +90,18 @@ pip install envbool
|
|
|
91
90
|
uv add envbool
|
|
92
91
|
```
|
|
93
92
|
|
|
93
|
+
### Snap
|
|
94
|
+
|
|
95
|
+
On Linux, the CLI is also available as a snap:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
sudo snap install envbool
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The snap is CLI-only; use `pip` or `uv` to `import envbool`. snapd overrides
|
|
102
|
+
`HOME`, `PATH`, `TMPDIR`, `XDG_RUNTIME_DIR`, and `SNAP_*`, so the CLI sees
|
|
103
|
+
snapd's values for those.
|
|
104
|
+
|
|
94
105
|
## Usage
|
|
95
106
|
|
|
96
107
|
### The basics
|
|
@@ -113,6 +124,9 @@ surrounding whitespace.
|
|
|
113
124
|
|
|
114
125
|
Pass `strict=True` to raise `InvalidBoolValueError` on anything outside the
|
|
115
126
|
truthy/falsy sets — ideal for failing fast on a misconfigured deployment.
|
|
127
|
+
Strict mode also raises `ConflictingValuesError` if the effective truthy and
|
|
128
|
+
falsy sets overlap (e.g. `extend_falsy={"on"}`), on every call, whatever the
|
|
129
|
+
value. Lenient mode instead logs a warning and lets truthy win.
|
|
116
130
|
|
|
117
131
|
```python
|
|
118
132
|
import sys
|
|
@@ -137,6 +151,16 @@ FEATURE = envbool("FEATURE_FLAG", extend_truthy={"enabled", "y"})
|
|
|
137
151
|
LOCALE = envbool("USE_METRIC", truthy={"metric"}, falsy={"imperial"})
|
|
138
152
|
```
|
|
139
153
|
|
|
154
|
+
Each set is built in order: start from the built-in set, swap it out if
|
|
155
|
+
`truthy`/`falsy` is given, then add anything in `extend_truthy`/
|
|
156
|
+
`extend_falsy`. Passing both `truthy` and `extend_truthy` therefore gives you
|
|
157
|
+
exactly their union:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
to_bool("y", truthy={"yes"}, extend_truthy={"y"}) # True
|
|
161
|
+
to_bool("true", truthy={"yes"}, extend_truthy={"y"}) # False: built-ins replaced
|
|
162
|
+
```
|
|
163
|
+
|
|
140
164
|
### Coercing arbitrary strings
|
|
141
165
|
|
|
142
166
|
Use `to_bool` for values that don't come from the environment. It accepts the
|
|
@@ -150,36 +174,45 @@ to_bool("0") # False
|
|
|
150
174
|
to_bool("maybe", strict=True) # raises InvalidBoolValueError
|
|
151
175
|
```
|
|
152
176
|
|
|
153
|
-
###
|
|
177
|
+
### Loading application settings
|
|
154
178
|
|
|
155
|
-
|
|
156
|
-
|
|
179
|
+
In a real application, read every flag once at startup into a single settings
|
|
180
|
+
object. With strict mode on, a typo like
|
|
181
|
+
`DEBUG=ture` stops startup instead of quietly reading as `False`:
|
|
157
182
|
|
|
158
183
|
```python
|
|
159
|
-
import
|
|
184
|
+
import sys
|
|
185
|
+
from dataclasses import dataclass
|
|
160
186
|
|
|
161
|
-
envbool
|
|
187
|
+
from envbool import EnvBoolError, envbool
|
|
162
188
|
|
|
163
|
-
envbool.envbool("DEBUG") # now raises on unrecognized values by default
|
|
164
|
-
```
|
|
165
189
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class Settings:
|
|
192
|
+
debug: bool
|
|
193
|
+
use_cache: bool
|
|
194
|
+
new_checkout: bool
|
|
195
|
+
send_emails: bool
|
|
170
196
|
|
|
171
|
-
```
|
|
172
|
-
built-in defaults → set_defaults() → function arguments / CLI flags
|
|
173
|
-
```
|
|
174
197
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
def load_settings() -> Settings:
|
|
199
|
+
return Settings(
|
|
200
|
+
debug=envbool("DEBUG", strict=True), # off unless set
|
|
201
|
+
use_cache=envbool("USE_CACHE", default=True, strict=True), # on unless set
|
|
202
|
+
new_checkout=envbool("FEATURE_NEW_CHECKOUT", strict=True),
|
|
203
|
+
send_emails=envbool("SEND_EMAILS", required=True, strict=True), # must be set
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
SETTINGS = load_settings()
|
|
209
|
+
except EnvBoolError as e:
|
|
210
|
+
sys.exit(f"Invalid configuration: {e}")
|
|
211
|
+
```
|
|
179
212
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
Catching `EnvBoolError` covers every failure: a bad value, a missing
|
|
214
|
+
`required` variable, or overlapping value sets. The rest of the application
|
|
215
|
+
reads `SETTINGS.debug` and never touches `os.environ` again.
|
|
183
216
|
|
|
184
217
|
## Command-line interface
|
|
185
218
|
|
|
@@ -227,35 +260,55 @@ options:
|
|
|
227
260
|
--truthy VALUE Replace the truthy set with VALUE (repeatable).
|
|
228
261
|
--falsy VALUE Replace the falsy set with VALUE (repeatable).
|
|
229
262
|
--extend-truthy VALUE
|
|
230
|
-
Add VALUE to the truthy set
|
|
231
|
-
|
|
263
|
+
Add VALUE to the truthy set, after any --truthy
|
|
264
|
+
(repeatable).
|
|
265
|
+
--extend-falsy VALUE Add VALUE to the falsy set, after any --falsy
|
|
266
|
+
(repeatable).
|
|
232
267
|
```
|
|
233
268
|
|
|
234
269
|
A few rules worth knowing:
|
|
235
270
|
|
|
236
|
-
-
|
|
237
|
-
warnings). `set_defaults()` is a library-level concern — the one-shot CLI
|
|
238
|
-
process doesn't read it.
|
|
271
|
+
- Without `--strict` / `--warn`, coercion is lenient and logs no warnings.
|
|
239
272
|
- `VAR_NAME` and `--value` are mutually exclusive.
|
|
240
273
|
- `--required` only applies to `VAR_NAME`; combining it with `--value` or
|
|
241
274
|
giving it no `VAR_NAME` at all is a usage error.
|
|
242
275
|
- With no `VAR_NAME`, `--value`, or non-empty piped stdin, the CLI prints
|
|
243
276
|
usage and exits `2`.
|
|
244
277
|
|
|
278
|
+
### Scripts using `set -e`
|
|
279
|
+
|
|
280
|
+
Under `set -e` (errexit), a falsy result is a failing command: a bare
|
|
281
|
+
`envbool FLAG` on its own line aborts the script when the flag is off. Check
|
|
282
|
+
the status inside a condition instead, where errexit doesn't apply, or use
|
|
283
|
+
`--print` to get the answer as text:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
set -e
|
|
287
|
+
|
|
288
|
+
envbool FLAG # aborts the script when FLAG is falsy
|
|
289
|
+
|
|
290
|
+
if envbool FLAG; then # safe: the status is the condition
|
|
291
|
+
echo "on"
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
flag=$(envbool --print FLAG) # safe: always exits 0 unless there's an error
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`--print` still exits `2` on an error, so `set -e` catches a missing
|
|
298
|
+
`--required` variable or a bad value under `--strict`, while a falsy value
|
|
299
|
+
doesn't stop the script.
|
|
300
|
+
|
|
245
301
|
## API reference
|
|
246
302
|
|
|
247
303
|
| Symbol | Description |
|
|
248
304
|
| --- | --- |
|
|
249
305
|
| `envbool(var, **opts)` | Read an environment variable and return `bool`. |
|
|
250
306
|
| `to_bool(value, **opts)` | Coerce a string to `bool`. |
|
|
251
|
-
| `set_defaults(**opts)` | Set process-level strict/warn/truthy/falsy defaults, replacing the built-ins. |
|
|
252
|
-
| `get_defaults()` | Return the active `Defaults`. |
|
|
253
|
-
| `reset_defaults()` | Restore built-in defaults. |
|
|
254
|
-
| `Defaults` | Frozen dataclass: `strict`, `warn`, `effective_truthy`, `effective_falsy`. |
|
|
255
307
|
| `DEFAULT_TRUTHY` | `frozenset` of the built-in truthy strings. |
|
|
256
308
|
| `DEFAULT_FALSY` | `frozenset` of the built-in falsy strings. |
|
|
257
309
|
| `EnvBoolError` | Base class for every exception the library raises. |
|
|
258
310
|
| `InvalidBoolValueError` | Raised in strict mode for unrecognized values. Also a `ValueError`. |
|
|
311
|
+
| `ConflictingValuesError` | Raised in strict mode when the truthy and falsy sets overlap. Also a `ValueError`. |
|
|
259
312
|
| `MissingEnvVarError` | Raised by `envbool(required=True)` when the variable is unset. Also a `KeyError`. |
|
|
260
313
|
|
|
261
314
|
`envbool()` and `to_bool()` share the same keyword-only options:
|
|
@@ -263,10 +316,10 @@ A few rules worth knowing:
|
|
|
263
316
|
| Option | Type | Default | Meaning |
|
|
264
317
|
| --- | --- | --- | --- |
|
|
265
318
|
| `default` | `bool` | `False` | Returned for unset/empty input. |
|
|
266
|
-
| `strict` | `bool
|
|
267
|
-
| `warn` | `bool
|
|
319
|
+
| `strict` | `bool` | `False` | Raise on unrecognized values. |
|
|
320
|
+
| `warn` | `bool` | `False` | Log a warning on unrecognized values. |
|
|
268
321
|
| `truthy` / `falsy` | `Iterable[str] \| None` | `None` | **Replace** the effective set. |
|
|
269
|
-
| `extend_truthy` / `extend_falsy` | `Iterable[str] \| None` | `None` | **Extend** the effective set. |
|
|
322
|
+
| `extend_truthy` / `extend_falsy` | `Iterable[str] \| None` | `None` | **Extend** the effective set, after any replacement. |
|
|
270
323
|
|
|
271
324
|
`envbool()` also accepts `required` (`bool`, default `False`): when `True`, a
|
|
272
325
|
variable that is unset raises `MissingEnvVarError` before `default` is applied. A
|
|
@@ -317,7 +370,7 @@ logging.getLogger("envbool").addHandler(logging.StreamHandler())
|
|
|
317
370
|
| Level | When |
|
|
318
371
|
| --- | --- |
|
|
319
372
|
| `WARNING` | An unrecognized value fell through in lenient mode (only when `warn=True`). |
|
|
320
|
-
| `WARNING` | The truthy and falsy sets overlap (truthy wins). |
|
|
373
|
+
| `WARNING` | The truthy and falsy sets overlap in lenient mode (truthy wins; strict mode raises `ConflictingValuesError` instead). |
|
|
321
374
|
|
|
322
375
|
### The unset-vs-empty distinction
|
|
323
376
|
|
|
@@ -336,23 +389,6 @@ else:
|
|
|
336
389
|
result = envbool("MY_VAR")
|
|
337
390
|
```
|
|
338
391
|
|
|
339
|
-
### Testing code that uses envbool
|
|
340
|
-
|
|
341
|
-
If your tests call `set_defaults()`, reset it between tests with an autouse
|
|
342
|
-
fixture so overrides don't leak across the suite:
|
|
343
|
-
|
|
344
|
-
```python
|
|
345
|
-
# conftest.py
|
|
346
|
-
import pytest
|
|
347
|
-
from envbool import reset_defaults
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
@pytest.fixture(autouse=True)
|
|
351
|
-
def _reset_envbool_defaults():
|
|
352
|
-
yield
|
|
353
|
-
reset_defaults()
|
|
354
|
-
```
|
|
355
|
-
|
|
356
392
|
## Contributing
|
|
357
393
|
|
|
358
394
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
**Coerce environment variables and strings into booleans — sensibly.**
|
|
6
6
|
|
|
7
7
|
[](https://pypi.org/project/envbool/)
|
|
8
|
+
[](https://snapcraft.io/envbool)
|
|
8
9
|
[](https://pypi.org/project/envbool/)
|
|
9
10
|
[](LICENSE)
|
|
10
11
|
[](https://github.com/jkomalley/envbool/actions/workflows/ci.yml)
|
|
@@ -39,8 +40,6 @@ CACHE = envbool("CACHE")
|
|
|
39
40
|
- **Always returns `bool`.** No `None`, no surprises in your type signatures.
|
|
40
41
|
- **Customizable value sets.** Replace or extend the truthy/falsy words your
|
|
41
42
|
environment uses.
|
|
42
|
-
- **Process-level defaults.** Call `set_defaults()` once at startup instead
|
|
43
|
-
of threading options through every call site.
|
|
44
43
|
- **A CLI for shell scripts.** Exit codes map to truthiness, so it drops
|
|
45
44
|
straight into `&&` / `||` chains.
|
|
46
45
|
- **Zero ceremony.** Zero dependencies, fully typed, Python 3.11+.
|
|
@@ -63,6 +62,18 @@ pip install envbool
|
|
|
63
62
|
uv add envbool
|
|
64
63
|
```
|
|
65
64
|
|
|
65
|
+
### Snap
|
|
66
|
+
|
|
67
|
+
On Linux, the CLI is also available as a snap:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
sudo snap install envbool
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The snap is CLI-only; use `pip` or `uv` to `import envbool`. snapd overrides
|
|
74
|
+
`HOME`, `PATH`, `TMPDIR`, `XDG_RUNTIME_DIR`, and `SNAP_*`, so the CLI sees
|
|
75
|
+
snapd's values for those.
|
|
76
|
+
|
|
66
77
|
## Usage
|
|
67
78
|
|
|
68
79
|
### The basics
|
|
@@ -85,6 +96,9 @@ surrounding whitespace.
|
|
|
85
96
|
|
|
86
97
|
Pass `strict=True` to raise `InvalidBoolValueError` on anything outside the
|
|
87
98
|
truthy/falsy sets — ideal for failing fast on a misconfigured deployment.
|
|
99
|
+
Strict mode also raises `ConflictingValuesError` if the effective truthy and
|
|
100
|
+
falsy sets overlap (e.g. `extend_falsy={"on"}`), on every call, whatever the
|
|
101
|
+
value. Lenient mode instead logs a warning and lets truthy win.
|
|
88
102
|
|
|
89
103
|
```python
|
|
90
104
|
import sys
|
|
@@ -109,6 +123,16 @@ FEATURE = envbool("FEATURE_FLAG", extend_truthy={"enabled", "y"})
|
|
|
109
123
|
LOCALE = envbool("USE_METRIC", truthy={"metric"}, falsy={"imperial"})
|
|
110
124
|
```
|
|
111
125
|
|
|
126
|
+
Each set is built in order: start from the built-in set, swap it out if
|
|
127
|
+
`truthy`/`falsy` is given, then add anything in `extend_truthy`/
|
|
128
|
+
`extend_falsy`. Passing both `truthy` and `extend_truthy` therefore gives you
|
|
129
|
+
exactly their union:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
to_bool("y", truthy={"yes"}, extend_truthy={"y"}) # True
|
|
133
|
+
to_bool("true", truthy={"yes"}, extend_truthy={"y"}) # False: built-ins replaced
|
|
134
|
+
```
|
|
135
|
+
|
|
112
136
|
### Coercing arbitrary strings
|
|
113
137
|
|
|
114
138
|
Use `to_bool` for values that don't come from the environment. It accepts the
|
|
@@ -122,36 +146,45 @@ to_bool("0") # False
|
|
|
122
146
|
to_bool("maybe", strict=True) # raises InvalidBoolValueError
|
|
123
147
|
```
|
|
124
148
|
|
|
125
|
-
###
|
|
149
|
+
### Loading application settings
|
|
126
150
|
|
|
127
|
-
|
|
128
|
-
|
|
151
|
+
In a real application, read every flag once at startup into a single settings
|
|
152
|
+
object. With strict mode on, a typo like
|
|
153
|
+
`DEBUG=ture` stops startup instead of quietly reading as `False`:
|
|
129
154
|
|
|
130
155
|
```python
|
|
131
|
-
import
|
|
156
|
+
import sys
|
|
157
|
+
from dataclasses import dataclass
|
|
132
158
|
|
|
133
|
-
envbool
|
|
159
|
+
from envbool import EnvBoolError, envbool
|
|
134
160
|
|
|
135
|
-
envbool.envbool("DEBUG") # now raises on unrecognized values by default
|
|
136
|
-
```
|
|
137
161
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class Settings:
|
|
164
|
+
debug: bool
|
|
165
|
+
use_cache: bool
|
|
166
|
+
new_checkout: bool
|
|
167
|
+
send_emails: bool
|
|
142
168
|
|
|
143
|
-
```
|
|
144
|
-
built-in defaults → set_defaults() → function arguments / CLI flags
|
|
145
|
-
```
|
|
146
169
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
def load_settings() -> Settings:
|
|
171
|
+
return Settings(
|
|
172
|
+
debug=envbool("DEBUG", strict=True), # off unless set
|
|
173
|
+
use_cache=envbool("USE_CACHE", default=True, strict=True), # on unless set
|
|
174
|
+
new_checkout=envbool("FEATURE_NEW_CHECKOUT", strict=True),
|
|
175
|
+
send_emails=envbool("SEND_EMAILS", required=True, strict=True), # must be set
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
SETTINGS = load_settings()
|
|
181
|
+
except EnvBoolError as e:
|
|
182
|
+
sys.exit(f"Invalid configuration: {e}")
|
|
183
|
+
```
|
|
151
184
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
185
|
+
Catching `EnvBoolError` covers every failure: a bad value, a missing
|
|
186
|
+
`required` variable, or overlapping value sets. The rest of the application
|
|
187
|
+
reads `SETTINGS.debug` and never touches `os.environ` again.
|
|
155
188
|
|
|
156
189
|
## Command-line interface
|
|
157
190
|
|
|
@@ -199,35 +232,55 @@ options:
|
|
|
199
232
|
--truthy VALUE Replace the truthy set with VALUE (repeatable).
|
|
200
233
|
--falsy VALUE Replace the falsy set with VALUE (repeatable).
|
|
201
234
|
--extend-truthy VALUE
|
|
202
|
-
Add VALUE to the truthy set
|
|
203
|
-
|
|
235
|
+
Add VALUE to the truthy set, after any --truthy
|
|
236
|
+
(repeatable).
|
|
237
|
+
--extend-falsy VALUE Add VALUE to the falsy set, after any --falsy
|
|
238
|
+
(repeatable).
|
|
204
239
|
```
|
|
205
240
|
|
|
206
241
|
A few rules worth knowing:
|
|
207
242
|
|
|
208
|
-
-
|
|
209
|
-
warnings). `set_defaults()` is a library-level concern — the one-shot CLI
|
|
210
|
-
process doesn't read it.
|
|
243
|
+
- Without `--strict` / `--warn`, coercion is lenient and logs no warnings.
|
|
211
244
|
- `VAR_NAME` and `--value` are mutually exclusive.
|
|
212
245
|
- `--required` only applies to `VAR_NAME`; combining it with `--value` or
|
|
213
246
|
giving it no `VAR_NAME` at all is a usage error.
|
|
214
247
|
- With no `VAR_NAME`, `--value`, or non-empty piped stdin, the CLI prints
|
|
215
248
|
usage and exits `2`.
|
|
216
249
|
|
|
250
|
+
### Scripts using `set -e`
|
|
251
|
+
|
|
252
|
+
Under `set -e` (errexit), a falsy result is a failing command: a bare
|
|
253
|
+
`envbool FLAG` on its own line aborts the script when the flag is off. Check
|
|
254
|
+
the status inside a condition instead, where errexit doesn't apply, or use
|
|
255
|
+
`--print` to get the answer as text:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
set -e
|
|
259
|
+
|
|
260
|
+
envbool FLAG # aborts the script when FLAG is falsy
|
|
261
|
+
|
|
262
|
+
if envbool FLAG; then # safe: the status is the condition
|
|
263
|
+
echo "on"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
flag=$(envbool --print FLAG) # safe: always exits 0 unless there's an error
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`--print` still exits `2` on an error, so `set -e` catches a missing
|
|
270
|
+
`--required` variable or a bad value under `--strict`, while a falsy value
|
|
271
|
+
doesn't stop the script.
|
|
272
|
+
|
|
217
273
|
## API reference
|
|
218
274
|
|
|
219
275
|
| Symbol | Description |
|
|
220
276
|
| --- | --- |
|
|
221
277
|
| `envbool(var, **opts)` | Read an environment variable and return `bool`. |
|
|
222
278
|
| `to_bool(value, **opts)` | Coerce a string to `bool`. |
|
|
223
|
-
| `set_defaults(**opts)` | Set process-level strict/warn/truthy/falsy defaults, replacing the built-ins. |
|
|
224
|
-
| `get_defaults()` | Return the active `Defaults`. |
|
|
225
|
-
| `reset_defaults()` | Restore built-in defaults. |
|
|
226
|
-
| `Defaults` | Frozen dataclass: `strict`, `warn`, `effective_truthy`, `effective_falsy`. |
|
|
227
279
|
| `DEFAULT_TRUTHY` | `frozenset` of the built-in truthy strings. |
|
|
228
280
|
| `DEFAULT_FALSY` | `frozenset` of the built-in falsy strings. |
|
|
229
281
|
| `EnvBoolError` | Base class for every exception the library raises. |
|
|
230
282
|
| `InvalidBoolValueError` | Raised in strict mode for unrecognized values. Also a `ValueError`. |
|
|
283
|
+
| `ConflictingValuesError` | Raised in strict mode when the truthy and falsy sets overlap. Also a `ValueError`. |
|
|
231
284
|
| `MissingEnvVarError` | Raised by `envbool(required=True)` when the variable is unset. Also a `KeyError`. |
|
|
232
285
|
|
|
233
286
|
`envbool()` and `to_bool()` share the same keyword-only options:
|
|
@@ -235,10 +288,10 @@ A few rules worth knowing:
|
|
|
235
288
|
| Option | Type | Default | Meaning |
|
|
236
289
|
| --- | --- | --- | --- |
|
|
237
290
|
| `default` | `bool` | `False` | Returned for unset/empty input. |
|
|
238
|
-
| `strict` | `bool
|
|
239
|
-
| `warn` | `bool
|
|
291
|
+
| `strict` | `bool` | `False` | Raise on unrecognized values. |
|
|
292
|
+
| `warn` | `bool` | `False` | Log a warning on unrecognized values. |
|
|
240
293
|
| `truthy` / `falsy` | `Iterable[str] \| None` | `None` | **Replace** the effective set. |
|
|
241
|
-
| `extend_truthy` / `extend_falsy` | `Iterable[str] \| None` | `None` | **Extend** the effective set. |
|
|
294
|
+
| `extend_truthy` / `extend_falsy` | `Iterable[str] \| None` | `None` | **Extend** the effective set, after any replacement. |
|
|
242
295
|
|
|
243
296
|
`envbool()` also accepts `required` (`bool`, default `False`): when `True`, a
|
|
244
297
|
variable that is unset raises `MissingEnvVarError` before `default` is applied. A
|
|
@@ -289,7 +342,7 @@ logging.getLogger("envbool").addHandler(logging.StreamHandler())
|
|
|
289
342
|
| Level | When |
|
|
290
343
|
| --- | --- |
|
|
291
344
|
| `WARNING` | An unrecognized value fell through in lenient mode (only when `warn=True`). |
|
|
292
|
-
| `WARNING` | The truthy and falsy sets overlap (truthy wins). |
|
|
345
|
+
| `WARNING` | The truthy and falsy sets overlap in lenient mode (truthy wins; strict mode raises `ConflictingValuesError` instead). |
|
|
293
346
|
|
|
294
347
|
### The unset-vs-empty distinction
|
|
295
348
|
|
|
@@ -308,23 +361,6 @@ else:
|
|
|
308
361
|
result = envbool("MY_VAR")
|
|
309
362
|
```
|
|
310
363
|
|
|
311
|
-
### Testing code that uses envbool
|
|
312
|
-
|
|
313
|
-
If your tests call `set_defaults()`, reset it between tests with an autouse
|
|
314
|
-
fixture so overrides don't leak across the suite:
|
|
315
|
-
|
|
316
|
-
```python
|
|
317
|
-
# conftest.py
|
|
318
|
-
import pytest
|
|
319
|
-
from envbool import reset_defaults
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
@pytest.fixture(autouse=True)
|
|
323
|
-
def _reset_envbool_defaults():
|
|
324
|
-
yield
|
|
325
|
-
reset_defaults()
|
|
326
|
-
```
|
|
327
|
-
|
|
328
364
|
## Contributing
|
|
329
365
|
|
|
330
366
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "envbool"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "A small Python library and CLI tool for coercing environment variables (and arbitrary strings) into boolean values."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Kyle O'Malley", email = "j.kyle.omalley@gmail.com" }]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""envbool -- coerce environment variables and strings into booleans.
|
|
2
|
+
|
|
3
|
+
Import everything you need directly from this package:
|
|
4
|
+
|
|
5
|
+
from envbool import envbool, to_bool, InvalidBoolValueError
|
|
6
|
+
|
|
7
|
+
For except clauses, envbool.exceptions is also importable by name:
|
|
8
|
+
|
|
9
|
+
from envbool.exceptions import InvalidBoolValueError
|
|
10
|
+
|
|
11
|
+
Available names:
|
|
12
|
+
envbool() -- read an env var and coerce to bool (primary API)
|
|
13
|
+
to_bool() -- coerce an arbitrary string to bool (no os.environ)
|
|
14
|
+
DEFAULT_TRUTHY -- built-in truthy set (frozenset)
|
|
15
|
+
DEFAULT_FALSY -- built-in falsy set (frozenset)
|
|
16
|
+
EnvBoolError -- base exception for all envbool errors
|
|
17
|
+
InvalidBoolValueError -- raised in strict mode for unrecognized values
|
|
18
|
+
ConflictingValuesError -- raised in strict mode when truthy/falsy overlap
|
|
19
|
+
MissingEnvVarError -- raised by envbool(required=True) when a var is unset
|
|
20
|
+
"""
|
|
21
|
+
# All implementation lives in private underscore-prefixed modules so the public
|
|
22
|
+
# surface can be reshaped without breaking imports. Do not import from _core,
|
|
23
|
+
# _env, or _cli directly.
|
|
24
|
+
|
|
25
|
+
from envbool._core import DEFAULT_FALSY, DEFAULT_TRUTHY, to_bool
|
|
26
|
+
from envbool._env import envbool
|
|
27
|
+
from envbool.exceptions import (
|
|
28
|
+
ConflictingValuesError,
|
|
29
|
+
EnvBoolError,
|
|
30
|
+
InvalidBoolValueError,
|
|
31
|
+
MissingEnvVarError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"DEFAULT_FALSY",
|
|
36
|
+
"DEFAULT_TRUTHY",
|
|
37
|
+
"ConflictingValuesError",
|
|
38
|
+
"EnvBoolError",
|
|
39
|
+
"InvalidBoolValueError",
|
|
40
|
+
"MissingEnvVarError",
|
|
41
|
+
"envbool",
|
|
42
|
+
"to_bool",
|
|
43
|
+
]
|
|
@@ -14,20 +14,15 @@ Exit codes:
|
|
|
14
14
|
2 -- error (unrecognized value in strict mode, unset VAR_NAME with --required,
|
|
15
15
|
bad arguments, multi-line stdin)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
(envbool.set_defaults(); default: lenient, no warnings).
|
|
17
|
+
Without --strict or --warn, coercion is lenient and emits no warnings.
|
|
19
18
|
|
|
20
19
|
Value sets: --truthy/--falsy (repeatable) replace the truthy/falsy set;
|
|
21
|
-
--extend-truthy/--extend-falsy (repeatable) add to it
|
|
22
|
-
|
|
20
|
+
--extend-truthy/--extend-falsy (repeatable) add to it, after any
|
|
21
|
+
replacement.
|
|
23
22
|
|
|
24
23
|
Public surface:
|
|
25
24
|
main() -- entry point registered as the "envbool" command
|
|
26
25
|
"""
|
|
27
|
-
# --strict and --warn use default=None rather than False so an absent flag
|
|
28
|
-
# passes None through to envbool()/to_bool(), which then defers to the
|
|
29
|
-
# process-level defaults instead of overriding a set_defaults(strict=True)/
|
|
30
|
-
# warn=True with False.
|
|
31
26
|
|
|
32
27
|
__all__ = ["main"]
|
|
33
28
|
|
|
@@ -36,7 +31,7 @@ import sys
|
|
|
36
31
|
|
|
37
32
|
from envbool._core import to_bool
|
|
38
33
|
from envbool._env import envbool
|
|
39
|
-
from envbool.exceptions import
|
|
34
|
+
from envbool.exceptions import EnvBoolError
|
|
40
35
|
|
|
41
36
|
|
|
42
37
|
def _build_parser() -> argparse.ArgumentParser:
|
|
@@ -63,18 +58,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
63
58
|
"--strict",
|
|
64
59
|
"-s",
|
|
65
60
|
action="store_true",
|
|
66
|
-
# None so an absent flag defers to the process-level defaults rather
|
|
67
|
-
# than overriding them with False.
|
|
68
|
-
# store_true with default=None gives: flag present -> True, absent -> None.
|
|
69
|
-
default=None,
|
|
70
61
|
help="Raise error on unrecognized values.",
|
|
71
62
|
)
|
|
72
63
|
parser.add_argument(
|
|
73
64
|
"--warn",
|
|
74
65
|
action="store_true",
|
|
75
|
-
# None so an absent flag defers to the process-level defaults rather
|
|
76
|
-
# than overriding them with False.
|
|
77
|
-
default=None,
|
|
78
66
|
help="Log a warning on unrecognized values.",
|
|
79
67
|
)
|
|
80
68
|
parser.add_argument(
|
|
@@ -114,13 +102,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
114
102
|
"--extend-truthy",
|
|
115
103
|
metavar="VALUE",
|
|
116
104
|
action="append",
|
|
117
|
-
help="Add VALUE to the truthy set (repeatable).",
|
|
105
|
+
help="Add VALUE to the truthy set, after any --truthy (repeatable).",
|
|
118
106
|
)
|
|
119
107
|
parser.add_argument(
|
|
120
108
|
"--extend-falsy",
|
|
121
109
|
metavar="VALUE",
|
|
122
110
|
action="append",
|
|
123
|
-
help="Add VALUE to the falsy set (repeatable).",
|
|
111
|
+
help="Add VALUE to the falsy set, after any --falsy (repeatable).",
|
|
124
112
|
)
|
|
125
113
|
return parser
|
|
126
114
|
|
|
@@ -213,7 +201,7 @@ def main() -> None:
|
|
|
213
201
|
# SystemExit, which is not caught here and so propagates as intended.
|
|
214
202
|
try:
|
|
215
203
|
result = _coerce_from_source(parser, args)
|
|
216
|
-
except
|
|
204
|
+
except EnvBoolError as e:
|
|
217
205
|
print(f"error: {e}", file=sys.stderr)
|
|
218
206
|
sys.exit(2)
|
|
219
207
|
|
|
@@ -1,35 +1,65 @@
|
|
|
1
1
|
"""Pure string-to-bool coercion with configurable truthy/falsy sets.
|
|
2
2
|
|
|
3
3
|
Public surface:
|
|
4
|
-
DEFAULT_TRUTHY -- the built-in truthy set
|
|
5
|
-
DEFAULT_FALSY -- the built-in falsy set
|
|
4
|
+
DEFAULT_TRUTHY -- the built-in truthy set
|
|
5
|
+
DEFAULT_FALSY -- the built-in falsy set
|
|
6
6
|
to_bool() -- coerce a single string to bool
|
|
7
7
|
|
|
8
8
|
Private surface (used by _env.py and tests):
|
|
9
|
-
_resolve() -- compute effective truthy/falsy sets from
|
|
9
|
+
_resolve() -- compute effective truthy/falsy sets from call-site args
|
|
10
10
|
"""
|
|
11
|
-
# This module has no knowledge of os.environ -- that lives in _env.py. It
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# to False.
|
|
11
|
+
# This module has no knowledge of os.environ -- that lives in _env.py. It also
|
|
12
|
+
# holds no process-wide state: every setting comes from the call-site
|
|
13
|
+
# arguments.
|
|
15
14
|
|
|
16
|
-
__all__ = ["to_bool"]
|
|
15
|
+
__all__ = ["DEFAULT_FALSY", "DEFAULT_TRUTHY", "to_bool"]
|
|
17
16
|
|
|
18
17
|
import logging
|
|
19
18
|
from collections.abc import Iterable
|
|
20
19
|
|
|
21
|
-
from envbool.
|
|
22
|
-
DEFAULT_FALSY,
|
|
23
|
-
DEFAULT_TRUTHY,
|
|
24
|
-
_apply_replace_or_extend,
|
|
25
|
-
get_defaults,
|
|
26
|
-
)
|
|
27
|
-
from envbool.exceptions import InvalidBoolValueError
|
|
20
|
+
from envbool.exceptions import ConflictingValuesError, InvalidBoolValueError
|
|
28
21
|
|
|
29
22
|
# Module-level logger -- attributed to "envbool._core" so callers can filter it
|
|
30
|
-
# independently from
|
|
23
|
+
# independently from the root "envbool" logger.
|
|
31
24
|
_logger = logging.getLogger(__name__)
|
|
32
25
|
|
|
26
|
+
DEFAULT_TRUTHY: frozenset[str] = frozenset({"true", "1", "yes", "on"})
|
|
27
|
+
DEFAULT_FALSY: frozenset[str] = frozenset({"false", "0", "no", "off"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalize_set(values: Iterable[str]) -> frozenset[str]:
|
|
31
|
+
"""Strip and lowercase values so they match to_bool()'s normalized input."""
|
|
32
|
+
return frozenset(v.strip().lower() for v in values)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _apply_replace_then_extend(
|
|
36
|
+
base: frozenset[str],
|
|
37
|
+
replace: Iterable[str] | None,
|
|
38
|
+
extend: Iterable[str] | None,
|
|
39
|
+
) -> frozenset[str]:
|
|
40
|
+
"""Resolve a value set: replace the base (if given), then extend the result.
|
|
41
|
+
|
|
42
|
+
Used by _resolve() for each of the truthy and falsy sets:
|
|
43
|
+
replace -- swaps out base entirely; the caller owns the starting set
|
|
44
|
+
extend -- additive; merged on top of whatever replace left
|
|
45
|
+
neither -- use base as-is
|
|
46
|
+
Passing both applies replace first, then extend, so neither argument is
|
|
47
|
+
silently dropped.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base: The starting set, used when replace is None.
|
|
51
|
+
replace: If not None, fully replaces base (normalized).
|
|
52
|
+
extend: If not None, merged on top of the (possibly replaced) set.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The resolved, normalized frozenset.
|
|
56
|
+
"""
|
|
57
|
+
result = _normalize_set(replace) if replace is not None else base
|
|
58
|
+
if extend is not None:
|
|
59
|
+
result |= _normalize_set(extend)
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
33
63
|
# Public API
|
|
34
64
|
|
|
35
65
|
|
|
@@ -37,8 +67,8 @@ def to_bool(
|
|
|
37
67
|
value: str,
|
|
38
68
|
*,
|
|
39
69
|
default: bool = False,
|
|
40
|
-
strict: bool
|
|
41
|
-
warn: bool
|
|
70
|
+
strict: bool = False,
|
|
71
|
+
warn: bool = False,
|
|
42
72
|
truthy: Iterable[str] | None = None,
|
|
43
73
|
falsy: Iterable[str] | None = None,
|
|
44
74
|
extend_truthy: Iterable[str] | None = None,
|
|
@@ -50,14 +80,14 @@ def to_bool(
|
|
|
50
80
|
Args:
|
|
51
81
|
value: The string to coerce.
|
|
52
82
|
default: Returned when value is empty or unset.
|
|
53
|
-
strict: Raise on unrecognized values.
|
|
54
|
-
|
|
55
|
-
warn: Log a warning on unrecognized values. None defers to
|
|
56
|
-
process-level defaults (set_defaults()) (default False).
|
|
83
|
+
strict: Raise on unrecognized values.
|
|
84
|
+
warn: Log a warning on unrecognized values.
|
|
57
85
|
truthy: Replaces the effective truthy set.
|
|
58
86
|
falsy: Replaces the effective falsy set.
|
|
59
|
-
extend_truthy: Extends the effective truthy set
|
|
60
|
-
|
|
87
|
+
extend_truthy: Extends the effective truthy set, after any truthy
|
|
88
|
+
replacement.
|
|
89
|
+
extend_falsy: Extends the effective falsy set, after any falsy
|
|
90
|
+
replacement.
|
|
61
91
|
_var: Internal - env var name for error messages when called via envbool().
|
|
62
92
|
|
|
63
93
|
Returns:
|
|
@@ -65,6 +95,8 @@ def to_bool(
|
|
|
65
95
|
|
|
66
96
|
Raises:
|
|
67
97
|
InvalidBoolValueError: In strict mode when value is unrecognized.
|
|
98
|
+
ConflictingValuesError: In strict mode when the effective truthy and
|
|
99
|
+
falsy sets overlap.
|
|
68
100
|
"""
|
|
69
101
|
# Normalize first so all comparisons are case- and whitespace-insensitive.
|
|
70
102
|
# Empty after normalization means "unset" -- return the caller's default
|
|
@@ -73,27 +105,30 @@ def to_bool(
|
|
|
73
105
|
if not normalized:
|
|
74
106
|
return default
|
|
75
107
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# precedence chain:
|
|
79
|
-
# hardcoded defaults (_defaults.py)
|
|
80
|
-
# -> process-level defaults (effective_truthy/effective_falsy already
|
|
81
|
-
# resolved there by set_defaults())
|
|
82
|
-
# -> call-site args (truthy/extend_truthy/falsy/extend_falsy)
|
|
83
|
-
defaults = get_defaults()
|
|
108
|
+
# Precedence is two-level: the built-in sets, then the call-site args
|
|
109
|
+
# (truthy/extend_truthy/falsy/extend_falsy) layered on top by _resolve.
|
|
84
110
|
effective_truthy, effective_falsy = _resolve(
|
|
85
|
-
config_truthy=defaults.effective_truthy,
|
|
86
|
-
config_falsy=defaults.effective_falsy,
|
|
87
111
|
truthy=truthy,
|
|
88
112
|
falsy=falsy,
|
|
89
113
|
extend_truthy=extend_truthy,
|
|
90
114
|
extend_falsy=extend_falsy,
|
|
91
115
|
)
|
|
92
116
|
|
|
93
|
-
# Overlapping sets are a
|
|
94
|
-
#
|
|
117
|
+
# Overlapping sets are a configuration mistake. Strict mode promises every
|
|
118
|
+
# accepted value is unambiguous, so it rejects the configuration outright --
|
|
119
|
+
# on every call, not just when the value lands in the overlap, so the
|
|
120
|
+
# mistake surfaces at the first strict read. Lenient mode warns so the
|
|
121
|
+
# problem is visible, then lets truthy win to stay predictable.
|
|
95
122
|
overlap = effective_truthy & effective_falsy
|
|
96
123
|
if overlap:
|
|
124
|
+
if strict:
|
|
125
|
+
err = ConflictingValuesError(
|
|
126
|
+
f"Truthy and falsy sets overlap: {', '.join(sorted(overlap))}"
|
|
127
|
+
)
|
|
128
|
+
err.overlap = overlap
|
|
129
|
+
err.truthy = effective_truthy
|
|
130
|
+
err.falsy = effective_falsy
|
|
131
|
+
raise err
|
|
97
132
|
_logger.warning(
|
|
98
133
|
"Overlapping truthy/falsy values (truthy wins): %s", sorted(overlap)
|
|
99
134
|
)
|
|
@@ -101,16 +136,12 @@ def to_bool(
|
|
|
101
136
|
if normalized in effective_truthy:
|
|
102
137
|
return True
|
|
103
138
|
|
|
104
|
-
# Falsy is checked after truthy so the overlap rule above
|
|
105
|
-
# without any extra branching.
|
|
139
|
+
# Falsy is checked after truthy so the lenient overlap rule above (truthy
|
|
140
|
+
# wins) is enforced without any extra branching.
|
|
106
141
|
if normalized in effective_falsy:
|
|
107
142
|
return False
|
|
108
143
|
|
|
109
|
-
|
|
110
|
-
# default; None defers to whatever set_defaults() last set (which defaults
|
|
111
|
-
# to False if set_defaults() was never called).
|
|
112
|
-
effective_strict = strict if strict is not None else defaults.strict
|
|
113
|
-
if effective_strict:
|
|
144
|
+
if strict:
|
|
114
145
|
truthy_list = ", ".join(sorted(effective_truthy))
|
|
115
146
|
falsy_list = ", ".join(sorted(effective_falsy))
|
|
116
147
|
# _var is threaded in by envbool() so the error message names the env
|
|
@@ -134,8 +165,7 @@ def to_bool(
|
|
|
134
165
|
err.falsy = effective_falsy
|
|
135
166
|
raise err
|
|
136
167
|
|
|
137
|
-
|
|
138
|
-
if effective_warn:
|
|
168
|
+
if warn:
|
|
139
169
|
_logger.warning("Unrecognized boolean value: %r", normalized)
|
|
140
170
|
|
|
141
171
|
# Lenient fallback: anything unrecognized is treated as falsy. This matches
|
|
@@ -148,16 +178,14 @@ def to_bool(
|
|
|
148
178
|
|
|
149
179
|
def _resolve(
|
|
150
180
|
*,
|
|
151
|
-
config_truthy: frozenset[str] = DEFAULT_TRUTHY,
|
|
152
|
-
config_falsy: frozenset[str] = DEFAULT_FALSY,
|
|
153
181
|
truthy: Iterable[str] | None = None,
|
|
154
182
|
falsy: Iterable[str] | None = None,
|
|
155
183
|
extend_truthy: Iterable[str] | None = None,
|
|
156
184
|
extend_falsy: Iterable[str] | None = None,
|
|
157
185
|
) -> tuple[frozenset[str], frozenset[str]]:
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
effective_truthy =
|
|
161
|
-
effective_falsy =
|
|
186
|
+
# Replace-then-extend, per set -- see the _apply_replace_then_extend()
|
|
187
|
+
# docstring for the full precedence rules.
|
|
188
|
+
effective_truthy = _apply_replace_then_extend(DEFAULT_TRUTHY, truthy, extend_truthy)
|
|
189
|
+
effective_falsy = _apply_replace_then_extend(DEFAULT_FALSY, falsy, extend_falsy)
|
|
162
190
|
|
|
163
191
|
return (effective_truthy, effective_falsy)
|
|
@@ -6,7 +6,7 @@ Public surface:
|
|
|
6
6
|
# This is the only layer in the package that touches os.environ. The split
|
|
7
7
|
# between _env.py and _core.py keeps os.environ access isolated here so that
|
|
8
8
|
# to_bool() can be tested without monkeypatching the environment.
|
|
9
|
-
# Delegation chain: envbool() -> to_bool() -> _resolve()
|
|
9
|
+
# Delegation chain: envbool() -> to_bool() -> _resolve()
|
|
10
10
|
|
|
11
11
|
__all__ = ["envbool"]
|
|
12
12
|
|
|
@@ -22,8 +22,8 @@ def envbool(
|
|
|
22
22
|
*,
|
|
23
23
|
default: bool = False,
|
|
24
24
|
required: bool = False,
|
|
25
|
-
strict: bool
|
|
26
|
-
warn: bool
|
|
25
|
+
strict: bool = False,
|
|
26
|
+
warn: bool = False,
|
|
27
27
|
truthy: Iterable[str] | None = None,
|
|
28
28
|
falsy: Iterable[str] | None = None,
|
|
29
29
|
extend_truthy: Iterable[str] | None = None,
|
|
@@ -37,20 +37,22 @@ def envbool(
|
|
|
37
37
|
required: When True, raise if the variable is not set at all. A truly
|
|
38
38
|
unset var raises before `default` is considered; a var set to an
|
|
39
39
|
empty string is "present" and still coerces via `default`.
|
|
40
|
-
strict: Raise on unrecognized values.
|
|
41
|
-
|
|
42
|
-
warn: Log a warning on unrecognized values. None defers to
|
|
43
|
-
process-level defaults (set_defaults()) (default False).
|
|
40
|
+
strict: Raise on unrecognized values.
|
|
41
|
+
warn: Log a warning on unrecognized values.
|
|
44
42
|
truthy: Replaces the effective truthy set.
|
|
45
43
|
falsy: Replaces the effective falsy set.
|
|
46
|
-
extend_truthy: Extends the effective truthy set
|
|
47
|
-
|
|
44
|
+
extend_truthy: Extends the effective truthy set, after any truthy
|
|
45
|
+
replacement.
|
|
46
|
+
extend_falsy: Extends the effective falsy set, after any falsy
|
|
47
|
+
replacement.
|
|
48
48
|
|
|
49
49
|
Returns:
|
|
50
50
|
True if the env var value is in the truthy set, False otherwise.
|
|
51
51
|
|
|
52
52
|
Raises:
|
|
53
53
|
InvalidBoolValueError: In strict mode when the value is unrecognized.
|
|
54
|
+
ConflictingValuesError: In strict mode when the effective truthy and
|
|
55
|
+
falsy sets overlap.
|
|
54
56
|
MissingEnvVarError: When required=True and the variable is unset.
|
|
55
57
|
"""
|
|
56
58
|
# `required` distinguishes "absent from the environment" from "set but empty"
|
|
@@ -8,6 +8,7 @@ which predates envbool adoption keeps working without changes.
|
|
|
8
8
|
Hierarchy:
|
|
9
9
|
EnvBoolError(Exception)
|
|
10
10
|
InvalidBoolValueError(EnvBoolError, ValueError)
|
|
11
|
+
ConflictingValuesError(EnvBoolError, ValueError)
|
|
11
12
|
MissingEnvVarError(EnvBoolError, KeyError)
|
|
12
13
|
"""
|
|
13
14
|
|
|
@@ -42,6 +43,28 @@ class InvalidBoolValueError(EnvBoolError, ValueError):
|
|
|
42
43
|
falsy: frozenset[str]
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
class ConflictingValuesError(EnvBoolError, ValueError):
|
|
47
|
+
"""Raised in strict mode when the effective truthy and falsy sets overlap.
|
|
48
|
+
|
|
49
|
+
This is a configuration error, not a bad input value -- which is why it is
|
|
50
|
+
distinct from InvalidBoolValueError. It is raised on every strict call while
|
|
51
|
+
the conflict exists (not only when the value lands in the overlap), so a
|
|
52
|
+
misconfiguration fails immediately instead of waiting for a colliding token.
|
|
53
|
+
ValueError inheritance matches InvalidBoolValueError, so a single
|
|
54
|
+
``except ValueError`` still covers every strict-mode failure.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Set by the raising code after construction (see InvalidBoolValueError for
|
|
58
|
+
# why attributes live here rather than in __init__).
|
|
59
|
+
|
|
60
|
+
# Values present in both sets.
|
|
61
|
+
overlap: frozenset[str]
|
|
62
|
+
|
|
63
|
+
# The effective truthy and falsy sets that conflicted.
|
|
64
|
+
truthy: frozenset[str]
|
|
65
|
+
falsy: frozenset[str]
|
|
66
|
+
|
|
67
|
+
|
|
45
68
|
class MissingEnvVarError(EnvBoolError, KeyError):
|
|
46
69
|
"""Raised by envbool(var, required=True) when var is not set in the environment.
|
|
47
70
|
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"""envbool -- coerce environment variables and strings into booleans.
|
|
2
|
-
|
|
3
|
-
Import everything you need directly from this package:
|
|
4
|
-
|
|
5
|
-
from envbool import envbool, to_bool, InvalidBoolValueError
|
|
6
|
-
|
|
7
|
-
For except clauses, envbool.exceptions is also importable by name:
|
|
8
|
-
|
|
9
|
-
from envbool.exceptions import InvalidBoolValueError
|
|
10
|
-
|
|
11
|
-
Available names:
|
|
12
|
-
envbool() -- read an env var and coerce to bool (primary API)
|
|
13
|
-
to_bool() -- coerce an arbitrary string to bool (no os.environ)
|
|
14
|
-
set_defaults() -- set process-level strict/warn/truthy/falsy defaults
|
|
15
|
-
get_defaults() -- inspect the active process-level Defaults
|
|
16
|
-
reset_defaults() -- restore built-in defaults (for test fixtures)
|
|
17
|
-
Defaults -- frozen dataclass returned by get_defaults()
|
|
18
|
-
DEFAULT_TRUTHY -- built-in truthy set (frozenset)
|
|
19
|
-
DEFAULT_FALSY -- built-in falsy set (frozenset)
|
|
20
|
-
EnvBoolError -- base exception for all envbool errors
|
|
21
|
-
InvalidBoolValueError -- raised in strict mode for unrecognized values
|
|
22
|
-
MissingEnvVarError -- raised by envbool(required=True) when a var is unset
|
|
23
|
-
"""
|
|
24
|
-
# All implementation lives in private underscore-prefixed modules so the public
|
|
25
|
-
# surface can be reshaped without breaking imports. Do not import from _core,
|
|
26
|
-
# _env, _config, _cli, or _defaults directly.
|
|
27
|
-
|
|
28
|
-
from envbool._core import to_bool
|
|
29
|
-
from envbool._defaults import (
|
|
30
|
-
DEFAULT_FALSY,
|
|
31
|
-
DEFAULT_TRUTHY,
|
|
32
|
-
Defaults,
|
|
33
|
-
get_defaults,
|
|
34
|
-
reset_defaults,
|
|
35
|
-
set_defaults,
|
|
36
|
-
)
|
|
37
|
-
from envbool._env import envbool
|
|
38
|
-
from envbool.exceptions import (
|
|
39
|
-
EnvBoolError,
|
|
40
|
-
InvalidBoolValueError,
|
|
41
|
-
MissingEnvVarError,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
__all__ = [
|
|
45
|
-
"DEFAULT_FALSY",
|
|
46
|
-
"DEFAULT_TRUTHY",
|
|
47
|
-
"Defaults",
|
|
48
|
-
"EnvBoolError",
|
|
49
|
-
"InvalidBoolValueError",
|
|
50
|
-
"MissingEnvVarError",
|
|
51
|
-
"envbool",
|
|
52
|
-
"get_defaults",
|
|
53
|
-
"reset_defaults",
|
|
54
|
-
"set_defaults",
|
|
55
|
-
"to_bool",
|
|
56
|
-
]
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
"""Built-in truthy/falsy sets, set-resolution helpers, and process-level defaults.
|
|
2
|
-
|
|
3
|
-
The Defaults instance held here is consulted by to_bool()/envbool() whenever a
|
|
4
|
-
call-site strict/warn argument is None or no truthy/falsy override is given.
|
|
5
|
-
|
|
6
|
-
Public surface:
|
|
7
|
-
DEFAULT_TRUTHY -- the built-in truthy set
|
|
8
|
-
DEFAULT_FALSY -- the built-in falsy set
|
|
9
|
-
Defaults -- frozen dataclass with resolved process-level settings
|
|
10
|
-
get_defaults() -- returns the active Defaults
|
|
11
|
-
set_defaults() -- replaces the active Defaults, built from the built-ins
|
|
12
|
-
reset_defaults() -- restores the built-in Defaults; for test fixtures
|
|
13
|
-
|
|
14
|
-
This is a leaf module (no imports from elsewhere in envbool) so _core.py can
|
|
15
|
-
import from it without a circular dependency.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
__all__ = [
|
|
19
|
-
"DEFAULT_FALSY",
|
|
20
|
-
"DEFAULT_TRUTHY",
|
|
21
|
-
"Defaults",
|
|
22
|
-
"get_defaults",
|
|
23
|
-
"reset_defaults",
|
|
24
|
-
"set_defaults",
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
import threading
|
|
28
|
-
from collections.abc import Iterable
|
|
29
|
-
from dataclasses import dataclass
|
|
30
|
-
|
|
31
|
-
DEFAULT_TRUTHY: frozenset[str] = frozenset({"true", "1", "yes", "on"})
|
|
32
|
-
DEFAULT_FALSY: frozenset[str] = frozenset({"false", "0", "no", "off"})
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _normalize_set(values: Iterable[str]) -> frozenset[str]:
|
|
36
|
-
"""Strip and lowercase values so they match to_bool()'s normalized input."""
|
|
37
|
-
return frozenset(v.strip().lower() for v in values)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _apply_replace_or_extend(
|
|
41
|
-
base: frozenset[str],
|
|
42
|
-
replace: Iterable[str] | None,
|
|
43
|
-
extend: Iterable[str] | None,
|
|
44
|
-
) -> frozenset[str]:
|
|
45
|
-
"""Resolve a value set using replace/extend/fall-back-to-base precedence.
|
|
46
|
-
|
|
47
|
-
Shared by _resolve() (call-site truthy/falsy args, in _core.py) and
|
|
48
|
-
set_defaults() (below) since both layer their inputs on top of a base set
|
|
49
|
-
using the same ruff select/extend-select pattern:
|
|
50
|
-
replace -- full replacement; caller owns the entire set
|
|
51
|
-
extend -- additive; merges on top of base
|
|
52
|
-
neither -- use base as-is
|
|
53
|
-
replace takes precedence over extend; both cannot apply at once.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
base: The starting set to fall back to or extend.
|
|
57
|
-
replace: If not None, fully replaces base (normalized).
|
|
58
|
-
extend: If not None and replace is None, merged on top of base.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
The resolved, normalized frozenset.
|
|
62
|
-
"""
|
|
63
|
-
if replace is not None:
|
|
64
|
-
return _normalize_set(replace)
|
|
65
|
-
if extend is not None:
|
|
66
|
-
return base | _normalize_set(extend)
|
|
67
|
-
return base
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@dataclass(frozen=True)
|
|
71
|
-
class Defaults:
|
|
72
|
-
"""Process-level defaults consulted when a call-site argument is None.
|
|
73
|
-
|
|
74
|
-
Attributes:
|
|
75
|
-
strict: When True, unrecognized values raise InvalidBoolValueError.
|
|
76
|
-
warn: When True, unrecognized values in lenient mode emit a WARNING log.
|
|
77
|
-
effective_truthy: Fully resolved truthy set (after extend/replace logic).
|
|
78
|
-
effective_falsy: Fully resolved falsy set (after extend/replace logic).
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
strict: bool = False
|
|
82
|
-
warn: bool = False
|
|
83
|
-
effective_truthy: frozenset[str] = DEFAULT_TRUTHY
|
|
84
|
-
effective_falsy: frozenset[str] = DEFAULT_FALSY
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class _DefaultsCache:
|
|
88
|
-
# Building a Defaults() is pure in-memory work (no disk I/O), so the cache
|
|
89
|
-
# starts pre-populated -- readers never need a "not built yet" branch. Only
|
|
90
|
-
# writers (set_defaults/reset_defaults) take the lock, so a concurrent
|
|
91
|
-
# writer can't produce a torn read.
|
|
92
|
-
|
|
93
|
-
value: Defaults = Defaults()
|
|
94
|
-
lock: threading.Lock = threading.Lock()
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
_cache = _DefaultsCache()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def get_defaults() -> Defaults:
|
|
101
|
-
"""Return the active process-level defaults.
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
The current Defaults -- built-ins, or whatever set_defaults() last set.
|
|
105
|
-
"""
|
|
106
|
-
return _cache.value
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _validated_tuple(name: str, values: Iterable[str] | None) -> tuple[str, ...] | None:
|
|
110
|
-
"""Materialize an Iterable[str] argument once, validating every member.
|
|
111
|
-
|
|
112
|
-
Iterables (e.g. generators) can only be consumed once; materializing here
|
|
113
|
-
(rather than validating then re-passing the original iterable) avoids
|
|
114
|
-
silently resolving to an empty set on the second pass.
|
|
115
|
-
|
|
116
|
-
Raises:
|
|
117
|
-
TypeError: If any member is not a str.
|
|
118
|
-
"""
|
|
119
|
-
if values is None:
|
|
120
|
-
return None
|
|
121
|
-
materialized = tuple(values)
|
|
122
|
-
for item in materialized:
|
|
123
|
-
if not isinstance(item, str):
|
|
124
|
-
raise TypeError(
|
|
125
|
-
f"{name} must be an iterable of strings, got {type(item).__name__!r}"
|
|
126
|
-
)
|
|
127
|
-
return materialized
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def set_defaults(
|
|
131
|
-
*,
|
|
132
|
-
strict: bool | None = None,
|
|
133
|
-
warn: bool | None = None,
|
|
134
|
-
truthy: Iterable[str] | None = None,
|
|
135
|
-
falsy: Iterable[str] | None = None,
|
|
136
|
-
extend_truthy: Iterable[str] | None = None,
|
|
137
|
-
extend_falsy: Iterable[str] | None = None,
|
|
138
|
-
) -> None:
|
|
139
|
-
"""Set process-level defaults consulted when a call-site argument is None.
|
|
140
|
-
|
|
141
|
-
Each call replaces the defaults starting from the hardcoded built-ins --
|
|
142
|
-
it does not merge with a previous set_defaults() call. Call this once at
|
|
143
|
-
application startup rather than threading strict=/truthy=/etc. through
|
|
144
|
-
every envbool()/to_bool() call site.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
strict: Raise on unrecognized values by default. None keeps the
|
|
148
|
-
built-in (False).
|
|
149
|
-
warn: Log a warning on unrecognized values by default. None keeps the
|
|
150
|
-
built-in (False).
|
|
151
|
-
truthy: Replaces the built-in truthy set.
|
|
152
|
-
falsy: Replaces the built-in falsy set.
|
|
153
|
-
extend_truthy: Extends the built-in truthy set.
|
|
154
|
-
extend_falsy: Extends the built-in falsy set.
|
|
155
|
-
|
|
156
|
-
Raises:
|
|
157
|
-
TypeError: If strict/warn are not bool, or truthy/falsy/extend_truthy/
|
|
158
|
-
extend_falsy contain a non-string member.
|
|
159
|
-
"""
|
|
160
|
-
if strict is not None and not isinstance(strict, bool):
|
|
161
|
-
raise TypeError(f"strict must be a bool, got {type(strict).__name__!r}")
|
|
162
|
-
if warn is not None and not isinstance(warn, bool):
|
|
163
|
-
raise TypeError(f"warn must be a bool, got {type(warn).__name__!r}")
|
|
164
|
-
|
|
165
|
-
truthy = _validated_tuple("truthy", truthy)
|
|
166
|
-
falsy = _validated_tuple("falsy", falsy)
|
|
167
|
-
extend_truthy = _validated_tuple("extend_truthy", extend_truthy)
|
|
168
|
-
extend_falsy = _validated_tuple("extend_falsy", extend_falsy)
|
|
169
|
-
|
|
170
|
-
new_defaults = Defaults(
|
|
171
|
-
strict=strict if strict is not None else False,
|
|
172
|
-
warn=warn if warn is not None else False,
|
|
173
|
-
effective_truthy=_apply_replace_or_extend(
|
|
174
|
-
DEFAULT_TRUTHY, truthy, extend_truthy
|
|
175
|
-
),
|
|
176
|
-
effective_falsy=_apply_replace_or_extend(DEFAULT_FALSY, falsy, extend_falsy),
|
|
177
|
-
)
|
|
178
|
-
with _cache.lock:
|
|
179
|
-
_cache.value = new_defaults
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def reset_defaults() -> None:
|
|
183
|
-
"""Restore built-in defaults, discarding any set_defaults() override.
|
|
184
|
-
|
|
185
|
-
Intended for test fixtures: call in an autouse fixture teardown so a
|
|
186
|
-
set_defaults() call in one test doesn't leak into the next.
|
|
187
|
-
"""
|
|
188
|
-
with _cache.lock:
|
|
189
|
-
_cache.value = Defaults()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|