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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: envbool
3
- Version: 0.4.2
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
  [![PyPI version](https://img.shields.io/pypi/v/envbool)](https://pypi.org/project/envbool/)
36
+ [![Snap Store](https://snapcraft.io/envbool/badge.svg)](https://snapcraft.io/envbool)
36
37
  [![Python versions](https://img.shields.io/pypi/pyversions/envbool)](https://pypi.org/project/envbool/)
37
38
  [![License: MIT](https://img.shields.io/github/license/jkomalley/envbool)](LICENSE)
38
39
  [![CI](https://github.com/jkomalley/envbool/actions/workflows/ci.yml/badge.svg)](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
- ### Process-level defaults
177
+ ### Loading application settings
154
178
 
155
- Set policy once at startup instead of threading `strict=`/`extend_truthy=`
156
- through every call site:
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 envbool
184
+ import sys
185
+ from dataclasses import dataclass
160
186
 
161
- envbool.set_defaults(strict=True, extend_truthy=["enabled"])
187
+ from envbool import EnvBoolError, envbool
162
188
 
163
- envbool.envbool("DEBUG") # now raises on unrecognized values by default
164
- ```
165
189
 
166
- `set_defaults()` replaces the process-level defaults **from the built-ins**,
167
- not from whatever a previous `set_defaults()` call left in place — call it
168
- once. Call-site arguments (`envbool("X", strict=False)`) still override
169
- whatever `set_defaults()` configured:
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
- `get_defaults()` returns the active `Defaults` (a frozen dataclass: `strict`,
176
- `warn`, `effective_truthy`, `effective_falsy`) for inspection.
177
- `reset_defaults()` restores the built-ins — call it in a test fixture (see
178
- [Testing code that uses envbool](#testing-code-that-uses-envbool)).
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
- > Through 0.3.x, envbool read TOML config files (`envbool.toml`,
181
- > `[tool.envbool]`). 0.4.0 removed them in favor of `set_defaults()` — see
182
- > `CHANGELOG.md` for the rationale and migration note.
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 (repeatable).
231
- --extend-falsy VALUE Add VALUE to the falsy set (repeatable).
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
- - Omitting `--strict` / `--warn` uses the built-in defaults (lenient, no
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 \| None` | `None` | Raise on unrecognized values (`None` defers to `set_defaults()`). |
267
- | `warn` | `bool \| None` | `None` | Log a warning on unrecognized values (`None` defers to `set_defaults()`). |
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
  [![PyPI version](https://img.shields.io/pypi/v/envbool)](https://pypi.org/project/envbool/)
8
+ [![Snap Store](https://snapcraft.io/envbool/badge.svg)](https://snapcraft.io/envbool)
8
9
  [![Python versions](https://img.shields.io/pypi/pyversions/envbool)](https://pypi.org/project/envbool/)
9
10
  [![License: MIT](https://img.shields.io/github/license/jkomalley/envbool)](LICENSE)
10
11
  [![CI](https://github.com/jkomalley/envbool/actions/workflows/ci.yml/badge.svg)](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
- ### Process-level defaults
149
+ ### Loading application settings
126
150
 
127
- Set policy once at startup instead of threading `strict=`/`extend_truthy=`
128
- through every call site:
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 envbool
156
+ import sys
157
+ from dataclasses import dataclass
132
158
 
133
- envbool.set_defaults(strict=True, extend_truthy=["enabled"])
159
+ from envbool import EnvBoolError, envbool
134
160
 
135
- envbool.envbool("DEBUG") # now raises on unrecognized values by default
136
- ```
137
161
 
138
- `set_defaults()` replaces the process-level defaults **from the built-ins**,
139
- not from whatever a previous `set_defaults()` call left in place — call it
140
- once. Call-site arguments (`envbool("X", strict=False)`) still override
141
- whatever `set_defaults()` configured:
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
- `get_defaults()` returns the active `Defaults` (a frozen dataclass: `strict`,
148
- `warn`, `effective_truthy`, `effective_falsy`) for inspection.
149
- `reset_defaults()` restores the built-ins — call it in a test fixture (see
150
- [Testing code that uses envbool](#testing-code-that-uses-envbool)).
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
- > Through 0.3.x, envbool read TOML config files (`envbool.toml`,
153
- > `[tool.envbool]`). 0.4.0 removed them in favor of `set_defaults()` — see
154
- > `CHANGELOG.md` for the rationale and migration note.
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 (repeatable).
203
- --extend-falsy VALUE Add VALUE to the falsy set (repeatable).
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
- - Omitting `--strict` / `--warn` uses the built-in defaults (lenient, no
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 \| None` | `None` | Raise on unrecognized values (`None` defers to `set_defaults()`). |
239
- | `warn` | `bool \| None` | `None` | Log a warning on unrecognized values (`None` defers to `set_defaults()`). |
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.4.2"
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
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "envbool"
3
- version = "0.4.2"
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
- Omitting --strict or --warn defers to the process-level defaults
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. Mirrors ruff's
22
- select/extend-select pattern.
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 InvalidBoolValueError, MissingEnvVarError
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 (InvalidBoolValueError, MissingEnvVarError) as e:
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 (from _defaults)
5
- DEFAULT_FALSY -- the built-in falsy set (from _defaults)
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 layered inputs
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 does
12
- # consult get_defaults() from _defaults.py so that strict=None/warn=None defer
13
- # to the process-level defaults (set_defaults()) rather than always defaulting
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._defaults import (
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 "envbool.config" or the root "envbool" logger.
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 | None = None,
41
- warn: bool | None = None,
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. None defers to process-level
54
- defaults (set_defaults()) (default False).
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
- extend_falsy: Extends the effective falsy set.
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
- # Read the process-level defaults (set once via set_defaults(), or the
77
- # built-ins if never called). _resolve then applies the full three-level
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 caller mistake, not a runtime error. Warn so the
94
- # problem is visible, then let truthy win to stay consistent and predictable.
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 is enforced
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
- # Three-state logic: True/False at the call site override the process-level
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
- effective_warn = warn if warn is not None else defaults.warn
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
- # Priority mirrors ruff's select/extend-select pattern -- see
159
- # _apply_replace_or_extend() docstring for the full precedence rules.
160
- effective_truthy = _apply_replace_or_extend(config_truthy, truthy, extend_truthy)
161
- effective_falsy = _apply_replace_or_extend(config_falsy, falsy, extend_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() (+ get_defaults())
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 | None = None,
26
- warn: bool | None = None,
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. None defers to process-level
41
- defaults (set_defaults()) (default False).
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
- extend_falsy: Extends the effective falsy set.
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