envbool 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: envbool
3
- Version: 0.2.0
3
+ Version: 0.3.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
7
7
  Author-email: Kyle O'Malley <j.kyle.omalley@gmail.com>
8
8
  License-Expression: MIT
9
- Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
12
  Classifier: Topic :: Utilities
@@ -242,11 +242,13 @@ Set `ENVBOOL_NO_CONFIG=1` to skip config discovery entirely.
242
242
  | `envbool(var, **opts)` | Read an environment variable and return `bool`. |
243
243
  | `to_bool(value, **opts)` | Coerce a string to `bool`. |
244
244
  | `load_config()` | Load and return the active `EnvBoolConfig` (cached). |
245
+ | `reload_config()` | Discard the cache, re-read the config file, and return the fresh `EnvBoolConfig`. |
245
246
  | `EnvBoolConfig` | Frozen dataclass: `strict`, `warn`, `effective_truthy`, `effective_falsy`, `source_path`. |
246
247
  | `DEFAULT_TRUTHY` | `frozenset` of the built-in truthy strings. |
247
248
  | `DEFAULT_FALSY` | `frozenset` of the built-in falsy strings. |
248
249
  | `EnvBoolError` | Base class for every exception the library raises. |
249
250
  | `InvalidBoolValueError` | Raised in strict mode for unrecognized values. Also a `ValueError`. |
251
+ | `MissingEnvVarError` | Raised by `envbool(required=True)` when the variable is unset. Also a `KeyError`. |
250
252
  | `ConfigError` | Raised when a config file is malformed. |
251
253
 
252
254
  `envbool()` and `to_bool()` share the same keyword-only options:
@@ -259,6 +261,10 @@ Set `ENVBOOL_NO_CONFIG=1` to skip config discovery entirely.
259
261
  | `truthy` / `falsy` | `Iterable[str] \| None` | `None` | **Replace** the effective set. |
260
262
  | `extend_truthy` / `extend_falsy` | `Iterable[str] \| None` | `None` | **Extend** the effective set. |
261
263
 
264
+ `envbool()` also accepts `required` (`bool`, default `False`): when `True`, a
265
+ variable that is unset raises `MissingEnvVarError` before `default` is applied. A
266
+ variable set to an empty string counts as present and still uses `default`.
267
+
262
268
  ## Advanced topics
263
269
 
264
270
  ### Exception handling
@@ -214,11 +214,13 @@ Set `ENVBOOL_NO_CONFIG=1` to skip config discovery entirely.
214
214
  | `envbool(var, **opts)` | Read an environment variable and return `bool`. |
215
215
  | `to_bool(value, **opts)` | Coerce a string to `bool`. |
216
216
  | `load_config()` | Load and return the active `EnvBoolConfig` (cached). |
217
+ | `reload_config()` | Discard the cache, re-read the config file, and return the fresh `EnvBoolConfig`. |
217
218
  | `EnvBoolConfig` | Frozen dataclass: `strict`, `warn`, `effective_truthy`, `effective_falsy`, `source_path`. |
218
219
  | `DEFAULT_TRUTHY` | `frozenset` of the built-in truthy strings. |
219
220
  | `DEFAULT_FALSY` | `frozenset` of the built-in falsy strings. |
220
221
  | `EnvBoolError` | Base class for every exception the library raises. |
221
222
  | `InvalidBoolValueError` | Raised in strict mode for unrecognized values. Also a `ValueError`. |
223
+ | `MissingEnvVarError` | Raised by `envbool(required=True)` when the variable is unset. Also a `KeyError`. |
222
224
  | `ConfigError` | Raised when a config file is malformed. |
223
225
 
224
226
  `envbool()` and `to_bool()` share the same keyword-only options:
@@ -231,6 +233,10 @@ Set `ENVBOOL_NO_CONFIG=1` to skip config discovery entirely.
231
233
  | `truthy` / `falsy` | `Iterable[str] \| None` | `None` | **Replace** the effective set. |
232
234
  | `extend_truthy` / `extend_falsy` | `Iterable[str] \| None` | `None` | **Extend** the effective set. |
233
235
 
236
+ `envbool()` also accepts `required` (`bool`, default `False`): when `True`, a
237
+ variable that is unset raises `MissingEnvVarError` before `default` is applied. A
238
+ variable set to an empty string counts as present and still uses `default`.
239
+
234
240
  ## Advanced topics
235
241
 
236
242
  ### Exception handling
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "envbool"
3
- version = "0.2.0"
3
+ version = "0.3.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" }]
@@ -11,7 +11,7 @@ dependencies = [
11
11
  "platformdirs>=4.9.6",
12
12
  ]
13
13
  classifiers = [
14
- "Development Status :: 3 - Alpha",
14
+ "Development Status :: 4 - Beta",
15
15
  "Intended Audience :: Developers",
16
16
  "Topic :: Software Development :: Libraries :: Python Modules",
17
17
  "Topic :: Utilities",
@@ -40,6 +40,7 @@ build-backend = "uv_build"
40
40
 
41
41
  [dependency-groups]
42
42
  dev = [
43
+ "hypothesis>=6.155.5",
43
44
  "pre-commit>=4.5.1",
44
45
  "pytest>=9.0.3",
45
46
  "pytest-cov>=7.1.0",
@@ -12,22 +12,29 @@ Available names:
12
12
  envbool() -- read an env var and coerce to bool (primary API)
13
13
  to_bool() -- coerce an arbitrary string to bool (no os.environ)
14
14
  load_config() -- inspect or preload the process-level config cache
15
+ reload_config() -- discard the cache and re-read the config file
15
16
  EnvBoolConfig -- frozen dataclass returned by load_config()
16
17
  DEFAULT_TRUTHY -- built-in truthy set (frozenset)
17
18
  DEFAULT_FALSY -- built-in falsy set (frozenset)
18
19
  EnvBoolError -- base exception for all envbool errors
19
20
  InvalidBoolValueError -- raised in strict mode for unrecognized values
21
+ MissingEnvVarError -- raised by envbool(required=True) when a var is unset
20
22
  ConfigError -- raised for malformed or unreadable config files
21
23
  """
22
24
  # All implementation lives in private underscore-prefixed modules so the public
23
25
  # surface can be reshaped without breaking imports. Do not import from _core,
24
26
  # _env, _config, _cli, or _defaults directly.
25
27
 
26
- from envbool._config import EnvBoolConfig, load_config
28
+ from envbool._config import EnvBoolConfig, load_config, reload_config
27
29
  from envbool._core import to_bool
28
30
  from envbool._defaults import DEFAULT_FALSY, DEFAULT_TRUTHY
29
31
  from envbool._env import envbool
30
- from envbool.exceptions import ConfigError, EnvBoolError, InvalidBoolValueError
32
+ from envbool.exceptions import (
33
+ ConfigError,
34
+ EnvBoolError,
35
+ InvalidBoolValueError,
36
+ MissingEnvVarError,
37
+ )
31
38
 
32
39
  __all__ = [
33
40
  "DEFAULT_FALSY",
@@ -36,7 +43,9 @@ __all__ = [
36
43
  "EnvBoolConfig",
37
44
  "EnvBoolError",
38
45
  "InvalidBoolValueError",
46
+ "MissingEnvVarError",
39
47
  "envbool",
40
48
  "load_config",
49
+ "reload_config",
41
50
  "to_bool",
42
51
  ]
@@ -0,0 +1,6 @@
1
+ """Enable ``python -m envbool`` as an alias for the installed ``envbool`` command."""
2
+
3
+ from envbool._cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -11,7 +11,8 @@ Input source (first match wins):
11
11
  Exit codes:
12
12
  0 -- truthy
13
13
  1 -- falsy or unset/empty
14
- 2 -- error (unrecognized value in strict mode, bad arguments, multi-line stdin)
14
+ 2 -- error (unrecognized value in strict mode, unset VAR_NAME with --required,
15
+ bad arguments, multi-line stdin)
15
16
 
16
17
  Omitting --strict or --warn defers to the config file setting (default:
17
18
  lenient, no warnings).
@@ -40,7 +41,7 @@ import sys
40
41
  from envbool._config import load_config
41
42
  from envbool._core import _resolve, to_bool
42
43
  from envbool._env import envbool
43
- from envbool.exceptions import ConfigError, InvalidBoolValueError
44
+ from envbool.exceptions import ConfigError, InvalidBoolValueError, MissingEnvVarError
44
45
 
45
46
 
46
47
  def _build_parser() -> argparse.ArgumentParser:
@@ -86,6 +87,13 @@ def _build_parser() -> argparse.ArgumentParser:
86
87
  default=False,
87
88
  help="Default value if unset/empty (default: false).",
88
89
  )
90
+ parser.add_argument(
91
+ "--required",
92
+ "-r",
93
+ action="store_true",
94
+ default=False,
95
+ help="Exit 2 if VAR_NAME is not set in the environment.",
96
+ )
89
97
  parser.add_argument(
90
98
  "--print",
91
99
  "-p",
@@ -146,12 +154,15 @@ def _print_config(args: argparse.Namespace) -> None:
146
154
  print(f"falsy: {', '.join(sorted(effective_falsy))}")
147
155
 
148
156
 
149
- def main() -> None:
150
- """Parse arguments, resolve the input source, and exit with the appropriate code."""
151
- # All coercion logic lives in _core.py; this function is pure I/O plumbing.
152
- parser = _build_parser()
153
- args = parser.parse_args()
157
+ def _coerce_from_source(
158
+ parser: argparse.ArgumentParser, args: argparse.Namespace
159
+ ) -> bool:
160
+ """Resolve the input source (--value, VAR_NAME, or stdin) and coerce it to bool.
154
161
 
162
+ Validates the mutually exclusive source flags, then dispatches to the matching
163
+ coercion call. Exits via parser.error()/sys.exit() for usage errors; the
164
+ coercion exceptions propagate to main()'s single error boundary.
165
+ """
155
166
  value_set_kwargs = {
156
167
  "truthy": args.truthy,
157
168
  "falsy": args.falsy,
@@ -159,6 +170,63 @@ def main() -> None:
159
170
  "extend_falsy": args.extend_falsy,
160
171
  }
161
172
 
173
+ # --value and VAR_NAME are mutually exclusive. Using argparse's built-in
174
+ # add_mutually_exclusive_group would place them in a separate usage section,
175
+ # which makes the help text harder to read, so we validate manually instead.
176
+ if args.value is not None and args.var is not None:
177
+ parser.error("VAR_NAME and --value are mutually exclusive")
178
+
179
+ # --required only governs the env-var lookup; a literal --value always
180
+ # has a value, so combining them is a usage error rather than a no-op.
181
+ if args.required and args.value is not None:
182
+ parser.error("--required and --value are mutually exclusive")
183
+
184
+ if args.value is not None:
185
+ return to_bool(
186
+ args.value,
187
+ strict=args.strict,
188
+ warn=args.warn,
189
+ default=args.default,
190
+ **value_set_kwargs,
191
+ )
192
+ if args.var is not None:
193
+ return envbool(
194
+ args.var,
195
+ strict=args.strict,
196
+ warn=args.warn,
197
+ default=args.default,
198
+ required=args.required,
199
+ **value_set_kwargs,
200
+ )
201
+ if not sys.stdin.isatty():
202
+ # Non-TTY stdin means the user piped or redirected input. Strip surrounding
203
+ # whitespace (handles the trailing newline echo adds) then reject anything
204
+ # with an embedded newline -- only a single value is meaningful here.
205
+ raw = sys.stdin.read().strip()
206
+ if "\n" in raw:
207
+ print(
208
+ "error: stdin must contain a single value, not multiple lines",
209
+ file=sys.stderr,
210
+ )
211
+ sys.exit(2)
212
+ return to_bool(
213
+ raw,
214
+ strict=args.strict,
215
+ warn=args.warn,
216
+ default=args.default,
217
+ **value_set_kwargs,
218
+ )
219
+
220
+ parser.print_usage(sys.stderr)
221
+ sys.exit(2)
222
+
223
+
224
+ def main() -> None:
225
+ """Parse arguments, resolve the input source, and exit with the appropriate code."""
226
+ # All coercion logic lives in _core.py; this function is pure I/O plumbing.
227
+ parser = _build_parser()
228
+ args = parser.parse_args()
229
+
162
230
  # Both --show-config and the coercion calls below trigger config-file loading,
163
231
  # so a malformed config can raise ConfigError from either path. A single error
164
232
  # boundary around both keeps that failure a clean "error: ..." exit rather than
@@ -180,50 +248,8 @@ def main() -> None:
180
248
  _print_config(args)
181
249
  sys.exit(0)
182
250
 
183
- # --value and VAR_NAME are mutually exclusive. Using argparse's built-in
184
- # add_mutually_exclusive_group would place them in a separate usage section,
185
- # which makes the help text harder to read, so we validate manually instead.
186
- if args.value is not None and args.var is not None:
187
- parser.error("VAR_NAME and --value are mutually exclusive")
188
-
189
- if args.value is not None:
190
- result = to_bool(
191
- args.value,
192
- strict=args.strict,
193
- warn=args.warn,
194
- default=args.default,
195
- **value_set_kwargs,
196
- )
197
- elif args.var is not None:
198
- result = envbool(
199
- args.var,
200
- strict=args.strict,
201
- warn=args.warn,
202
- default=args.default,
203
- **value_set_kwargs,
204
- )
205
- elif not sys.stdin.isatty():
206
- # Non-TTY stdin means the user piped or redirected input. Strip surrounding
207
- # whitespace (handles the trailing newline echo adds) then reject anything
208
- # with an embedded newline -- only a single value is meaningful here.
209
- raw = sys.stdin.read().strip()
210
- if "\n" in raw:
211
- print(
212
- "error: stdin must contain a single value, not multiple lines",
213
- file=sys.stderr,
214
- )
215
- sys.exit(2)
216
- result = to_bool(
217
- raw,
218
- strict=args.strict,
219
- warn=args.warn,
220
- default=args.default,
221
- **value_set_kwargs,
222
- )
223
- else:
224
- parser.print_usage(sys.stderr)
225
- sys.exit(2)
226
- except (InvalidBoolValueError, ConfigError) as e:
251
+ result = _coerce_from_source(parser, args)
252
+ except (InvalidBoolValueError, ConfigError, MissingEnvVarError) as e:
227
253
  print(f"error: {e}", file=sys.stderr)
228
254
  sys.exit(2)
229
255
 
@@ -26,7 +26,7 @@ import platformdirs
26
26
  from envbool._defaults import DEFAULT_FALSY, DEFAULT_TRUTHY
27
27
  from envbool.exceptions import ConfigError
28
28
 
29
- __all__ = ["EnvBoolConfig", "load_config"]
29
+ __all__ = ["EnvBoolConfig", "load_config", "reload_config"]
30
30
 
31
31
  _logger = logging.getLogger(__name__)
32
32
 
@@ -89,6 +89,28 @@ def load_config() -> EnvBoolConfig:
89
89
  return _get_config()
90
90
 
91
91
 
92
+ def reload_config() -> EnvBoolConfig:
93
+ """Force a re-read of the config file, replacing the cached config.
94
+
95
+ ``load_config()`` caches for the lifetime of the process, so a config file
96
+ that is written or edited after the first lookup is never picked up. Call
97
+ this to discard the cache and reload from disk -- useful in long-running
98
+ processes (workers, servers, notebooks) that change config at runtime.
99
+
100
+ The clear-and-reload happens atomically under the cache lock so concurrent
101
+ readers never observe a momentarily empty cache.
102
+
103
+ Returns:
104
+ The freshly loaded EnvBoolConfig, now installed as the cached instance.
105
+
106
+ Raises:
107
+ ConfigError: If a config file is found but malformed or has invalid values.
108
+ """
109
+ with _cache.lock:
110
+ _cache.config = _load_config_from_disk()
111
+ return _cache.config
112
+
113
+
92
114
  def _get_config() -> EnvBoolConfig:
93
115
  # Double-checked locking: the outer check avoids lock contention on the hot
94
116
  # path (all calls after first load); the inner check prevents duplicate disk
@@ -14,12 +14,14 @@ import os
14
14
  from collections.abc import Iterable
15
15
 
16
16
  from envbool._core import to_bool
17
+ from envbool.exceptions import MissingEnvVarError
17
18
 
18
19
 
19
20
  def envbool(
20
21
  var: str,
21
22
  *,
22
23
  default: bool = False,
24
+ required: bool = False,
23
25
  strict: bool | None = None,
24
26
  warn: bool | None = None,
25
27
  truthy: Iterable[str] | None = None,
@@ -32,6 +34,9 @@ def envbool(
32
34
  Args:
33
35
  var: Environment variable name.
34
36
  default: Returned when the variable is unset or empty.
37
+ required: When True, raise if the variable is not set at all. A truly
38
+ unset var raises before `default` is considered; a var set to an
39
+ empty string is "present" and still coerces via `default`.
35
40
  strict: Raise on unrecognized values. None defers to config (default False).
36
41
  warn: Log a warning on unrecognized values. None defers to config
37
42
  (default False).
@@ -45,7 +50,17 @@ def envbool(
45
50
 
46
51
  Raises:
47
52
  InvalidBoolValueError: In strict mode when the value is unrecognized.
53
+ MissingEnvVarError: When required=True and the variable is unset.
48
54
  """
55
+ # `required` distinguishes "absent from the environment" from "set but empty"
56
+ # -- membership, not the value, so VAR= (empty) is treated as present. This
57
+ # check precedes the default-handling below so an unset required var raises
58
+ # instead of silently falling back to `default`.
59
+ if required and var not in os.environ:
60
+ err = MissingEnvVarError(f"Required environment variable {var} is not set")
61
+ err.var = var
62
+ raise err
63
+
49
64
  # Missing var becomes "" so to_bool treats it the same as an empty value,
50
65
  # returning `default` rather than raising or treating absence as a distinct state.
51
66
  value = os.environ.get(var, "")
@@ -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
+ MissingEnvVarError(EnvBoolError, KeyError)
11
12
  ConfigError(EnvBoolError)
12
13
  """
13
14
 
@@ -44,6 +45,24 @@ class InvalidBoolValueError(EnvBoolError, ValueError):
44
45
  falsy: frozenset[str]
45
46
 
46
47
 
48
+ class MissingEnvVarError(EnvBoolError, KeyError):
49
+ """Raised by envbool(var, required=True) when var is not set in the environment.
50
+
51
+ Dual inheritance with KeyError mirrors the InvalidBoolValueError(ValueError)
52
+ pattern: a missing os.environ lookup naturally raises KeyError, so existing
53
+ ``except KeyError`` handlers keep working after a codebase adopts envbool.
54
+
55
+ Only a truly-unset variable triggers this -- a variable set to an empty
56
+ string is "present" and coerces normally via the caller's default.
57
+ """
58
+
59
+ # Set by the raising code after construction (see InvalidBoolValueError for
60
+ # why attributes live here rather than in __init__).
61
+
62
+ # Name of the environment variable that was required but not set.
63
+ var: str
64
+
65
+
47
66
  class ConfigError(EnvBoolError):
48
67
  """Raised when a config file is malformed or contains invalid values.
49
68
 
File without changes
File without changes