envproof 0.1.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.
- envproof-0.1.0/.github/workflows/ci.yml +23 -0
- envproof-0.1.0/.gitignore +7 -0
- envproof-0.1.0/PKG-INFO +116 -0
- envproof-0.1.0/README.md +94 -0
- envproof-0.1.0/envguard/__init__.py +4 -0
- envproof-0.1.0/envguard/core.py +84 -0
- envproof-0.1.0/pyproject.toml +36 -0
- envproof-0.1.0/tests/__init__.py +0 -0
- envproof-0.1.0/tests/test_core.py +203 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- run: pip install pytest hatchling
|
|
22
|
+
- run: pip install -e .
|
|
23
|
+
- run: pytest
|
envproof-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: envproof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Validate environment variables at startup with typed, clear error messages. Zero dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/CoderSufiyan/envguard
|
|
6
|
+
Project-URL: Repository, https://github.com/CoderSufiyan/envguard
|
|
7
|
+
Project-URL: Issues, https://github.com/CoderSufiyan/envguard/issues
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: config,dotenv,env,environment,settings,validation
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# envguard
|
|
24
|
+
|
|
25
|
+
Validate environment variables at startup with typed, clear error messages. Zero dependencies.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from envguard import EnvGuard
|
|
29
|
+
|
|
30
|
+
class Env(EnvGuard):
|
|
31
|
+
DATABASE_URL: str
|
|
32
|
+
PORT: int = 8080
|
|
33
|
+
DEBUG: bool = False
|
|
34
|
+
ALLOWED_HOSTS: list = []
|
|
35
|
+
|
|
36
|
+
env = Env()
|
|
37
|
+
print(env.PORT) # 8080 (int, not string)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If `DATABASE_URL` is missing, you get this instead of a cryptic `KeyError` somewhere deep in your app:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
EnvGuardError:
|
|
44
|
+
Missing required environment variables:
|
|
45
|
+
- DATABASE_URL (str): not set
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install envguard
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Subclass style (recommended)
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from envguard import EnvGuard
|
|
60
|
+
|
|
61
|
+
class Env(EnvGuard):
|
|
62
|
+
# Required — raises if not set
|
|
63
|
+
DATABASE_URL: str
|
|
64
|
+
API_KEY: str
|
|
65
|
+
|
|
66
|
+
# Optional — uses default if not set
|
|
67
|
+
PORT: int = 8080
|
|
68
|
+
DEBUG: bool = False
|
|
69
|
+
LOG_LEVEL: str = "INFO"
|
|
70
|
+
ALLOWED_HOSTS: list = []
|
|
71
|
+
|
|
72
|
+
env = Env()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### One-liner style
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from envguard import guard
|
|
79
|
+
|
|
80
|
+
env = guard(DATABASE_URL=str, PORT=int, DEBUG=bool)
|
|
81
|
+
print(env.DATABASE_URL)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Supported types
|
|
85
|
+
|
|
86
|
+
| Type | Example env value | Python value |
|
|
87
|
+
|---------|---------------------------|--------------------------|
|
|
88
|
+
| `str` | `"hello"` | `"hello"` |
|
|
89
|
+
| `int` | `"8080"` | `8080` |
|
|
90
|
+
| `float` | `"3.14"` | `3.14` |
|
|
91
|
+
| `bool` | `"true"`, `"1"`, `"yes"` | `True` |
|
|
92
|
+
| `bool` | `"false"`, `"0"`, `"no"` | `False` |
|
|
93
|
+
| `list` | `"a,b,c"` | `["a", "b", "c"]` |
|
|
94
|
+
|
|
95
|
+
## Error messages
|
|
96
|
+
|
|
97
|
+
All errors are collected and reported together — you won't fix one missing var only to discover another:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
EnvGuardError:
|
|
101
|
+
Missing required environment variables:
|
|
102
|
+
- DATABASE_URL (str): not set
|
|
103
|
+
- API_KEY (str): not set
|
|
104
|
+
|
|
105
|
+
Invalid environment variable values:
|
|
106
|
+
- PORT: expected int, got 'abc'
|
|
107
|
+
- DEBUG: expected bool, got 'maybe'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Why not pydantic-settings?
|
|
111
|
+
|
|
112
|
+
`pydantic-settings` is great but pulls in Pydantic as a dependency (~2MB). `envguard` is a single file with zero dependencies — useful when you want validation without adding weight to your project.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
envproof-0.1.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# envguard
|
|
2
|
+
|
|
3
|
+
Validate environment variables at startup with typed, clear error messages. Zero dependencies.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from envguard import EnvGuard
|
|
7
|
+
|
|
8
|
+
class Env(EnvGuard):
|
|
9
|
+
DATABASE_URL: str
|
|
10
|
+
PORT: int = 8080
|
|
11
|
+
DEBUG: bool = False
|
|
12
|
+
ALLOWED_HOSTS: list = []
|
|
13
|
+
|
|
14
|
+
env = Env()
|
|
15
|
+
print(env.PORT) # 8080 (int, not string)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If `DATABASE_URL` is missing, you get this instead of a cryptic `KeyError` somewhere deep in your app:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
EnvGuardError:
|
|
22
|
+
Missing required environment variables:
|
|
23
|
+
- DATABASE_URL (str): not set
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install envguard
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Subclass style (recommended)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from envguard import EnvGuard
|
|
38
|
+
|
|
39
|
+
class Env(EnvGuard):
|
|
40
|
+
# Required — raises if not set
|
|
41
|
+
DATABASE_URL: str
|
|
42
|
+
API_KEY: str
|
|
43
|
+
|
|
44
|
+
# Optional — uses default if not set
|
|
45
|
+
PORT: int = 8080
|
|
46
|
+
DEBUG: bool = False
|
|
47
|
+
LOG_LEVEL: str = "INFO"
|
|
48
|
+
ALLOWED_HOSTS: list = []
|
|
49
|
+
|
|
50
|
+
env = Env()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### One-liner style
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from envguard import guard
|
|
57
|
+
|
|
58
|
+
env = guard(DATABASE_URL=str, PORT=int, DEBUG=bool)
|
|
59
|
+
print(env.DATABASE_URL)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Supported types
|
|
63
|
+
|
|
64
|
+
| Type | Example env value | Python value |
|
|
65
|
+
|---------|---------------------------|--------------------------|
|
|
66
|
+
| `str` | `"hello"` | `"hello"` |
|
|
67
|
+
| `int` | `"8080"` | `8080` |
|
|
68
|
+
| `float` | `"3.14"` | `3.14` |
|
|
69
|
+
| `bool` | `"true"`, `"1"`, `"yes"` | `True` |
|
|
70
|
+
| `bool` | `"false"`, `"0"`, `"no"` | `False` |
|
|
71
|
+
| `list` | `"a,b,c"` | `["a", "b", "c"]` |
|
|
72
|
+
|
|
73
|
+
## Error messages
|
|
74
|
+
|
|
75
|
+
All errors are collected and reported together — you won't fix one missing var only to discover another:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
EnvGuardError:
|
|
79
|
+
Missing required environment variables:
|
|
80
|
+
- DATABASE_URL (str): not set
|
|
81
|
+
- API_KEY (str): not set
|
|
82
|
+
|
|
83
|
+
Invalid environment variable values:
|
|
84
|
+
- PORT: expected int, got 'abc'
|
|
85
|
+
- DEBUG: expected bool, got 'maybe'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Why not pydantic-settings?
|
|
89
|
+
|
|
90
|
+
`pydantic-settings` is great but pulls in Pydantic as a dependency (~2MB). `envguard` is a single file with zero dependencies — useful when you want validation without adding weight to your project.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, get_type_hints
|
|
3
|
+
|
|
4
|
+
_MISSING = object()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EnvGuardError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EnvGuard:
|
|
12
|
+
"""
|
|
13
|
+
Subclass this and declare your env vars as type-annotated class attributes.
|
|
14
|
+
|
|
15
|
+
Required var: DATABASE_URL: str
|
|
16
|
+
Optional var: PORT: int = 8080
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
hints = get_type_hints(self.__class__)
|
|
21
|
+
missing = []
|
|
22
|
+
invalid = []
|
|
23
|
+
|
|
24
|
+
for name, type_ in hints.items():
|
|
25
|
+
if name.startswith("_"):
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
raw = os.environ.get(name)
|
|
29
|
+
has_default = name in self.__class__.__dict__
|
|
30
|
+
default = self.__class__.__dict__.get(name, _MISSING)
|
|
31
|
+
|
|
32
|
+
if raw is None:
|
|
33
|
+
if not has_default:
|
|
34
|
+
missing.append((name, type_))
|
|
35
|
+
else:
|
|
36
|
+
setattr(self, name, default)
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
setattr(self, name, _coerce(raw, type_))
|
|
41
|
+
except (ValueError, TypeError):
|
|
42
|
+
invalid.append((name, type_.__name__, raw))
|
|
43
|
+
|
|
44
|
+
errors = []
|
|
45
|
+
if missing:
|
|
46
|
+
errors.append("Missing required environment variables:")
|
|
47
|
+
for name, type_ in missing:
|
|
48
|
+
errors.append(f" - {name} ({type_.__name__}): not set")
|
|
49
|
+
if invalid:
|
|
50
|
+
if errors:
|
|
51
|
+
errors.append("")
|
|
52
|
+
errors.append("Invalid environment variable values:")
|
|
53
|
+
for name, type_name, raw in invalid:
|
|
54
|
+
errors.append(f" - {name}: expected {type_name}, got '{raw}'")
|
|
55
|
+
|
|
56
|
+
if errors:
|
|
57
|
+
raise EnvGuardError("\n" + "\n".join(errors))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _coerce(value: str, type_: type) -> Any:
|
|
61
|
+
if type_ is bool:
|
|
62
|
+
if value.lower() in ("true", "1", "yes", "on"):
|
|
63
|
+
return True
|
|
64
|
+
if value.lower() in ("false", "0", "no", "off"):
|
|
65
|
+
return False
|
|
66
|
+
raise ValueError(f"Cannot convert to bool: {value!r}")
|
|
67
|
+
if type_ is list:
|
|
68
|
+
return [v.strip() for v in value.split(",") if v.strip()]
|
|
69
|
+
return type_(value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def guard(**schema) -> Any:
|
|
73
|
+
"""
|
|
74
|
+
One-liner usage without subclassing:
|
|
75
|
+
|
|
76
|
+
env = guard(DATABASE_URL=str, PORT=int, DEBUG=bool)
|
|
77
|
+
print(env.DATABASE_URL)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
class _Env(EnvGuard):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
_Env.__annotations__ = schema
|
|
84
|
+
return _Env()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "envproof"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Validate environment variables at startup with typed, clear error messages. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["environment", "config", "validation", "env", "dotenv", "settings"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.8",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/CoderSufiyan/envguard"
|
|
29
|
+
Repository = "https://github.com/CoderSufiyan/envguard"
|
|
30
|
+
Issues = "https://github.com/CoderSufiyan/envguard/issues"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["envguard"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from envguard import EnvGuard, EnvGuardError, guard
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def set_env(**kwargs):
|
|
7
|
+
for k, v in kwargs.items():
|
|
8
|
+
os.environ[k] = str(v)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def clear_env(*keys):
|
|
12
|
+
for k in keys:
|
|
13
|
+
os.environ.pop(k, None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# --- EnvGuard subclass style ---
|
|
17
|
+
|
|
18
|
+
class TestRequired:
|
|
19
|
+
def test_loads_required_str(self):
|
|
20
|
+
set_env(APP_NAME="myapp")
|
|
21
|
+
|
|
22
|
+
class Env(EnvGuard):
|
|
23
|
+
APP_NAME: str
|
|
24
|
+
|
|
25
|
+
env = Env()
|
|
26
|
+
assert env.APP_NAME == "myapp"
|
|
27
|
+
clear_env("APP_NAME")
|
|
28
|
+
|
|
29
|
+
def test_loads_required_int(self):
|
|
30
|
+
set_env(PORT="9000")
|
|
31
|
+
|
|
32
|
+
class Env(EnvGuard):
|
|
33
|
+
PORT: int
|
|
34
|
+
|
|
35
|
+
env = Env()
|
|
36
|
+
assert env.PORT == 9000
|
|
37
|
+
clear_env("PORT")
|
|
38
|
+
|
|
39
|
+
def test_loads_required_float(self):
|
|
40
|
+
set_env(RATE="3.14")
|
|
41
|
+
|
|
42
|
+
class Env(EnvGuard):
|
|
43
|
+
RATE: float
|
|
44
|
+
|
|
45
|
+
env = Env()
|
|
46
|
+
assert env.RATE == 3.14
|
|
47
|
+
clear_env("RATE")
|
|
48
|
+
|
|
49
|
+
def test_raises_on_missing_required(self):
|
|
50
|
+
clear_env("DATABASE_URL")
|
|
51
|
+
|
|
52
|
+
class Env(EnvGuard):
|
|
53
|
+
DATABASE_URL: str
|
|
54
|
+
|
|
55
|
+
with pytest.raises(EnvGuardError) as exc:
|
|
56
|
+
Env()
|
|
57
|
+
assert "DATABASE_URL" in str(exc.value)
|
|
58
|
+
assert "Missing" in str(exc.value)
|
|
59
|
+
|
|
60
|
+
def test_raises_on_multiple_missing(self):
|
|
61
|
+
clear_env("DB_URL", "API_KEY")
|
|
62
|
+
|
|
63
|
+
class Env(EnvGuard):
|
|
64
|
+
DB_URL: str
|
|
65
|
+
API_KEY: str
|
|
66
|
+
|
|
67
|
+
with pytest.raises(EnvGuardError) as exc:
|
|
68
|
+
Env()
|
|
69
|
+
assert "DB_URL" in str(exc.value)
|
|
70
|
+
assert "API_KEY" in str(exc.value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestOptional:
|
|
74
|
+
def test_uses_default_when_not_set(self):
|
|
75
|
+
clear_env("PORT")
|
|
76
|
+
|
|
77
|
+
class Env(EnvGuard):
|
|
78
|
+
PORT: int = 8080
|
|
79
|
+
|
|
80
|
+
env = Env()
|
|
81
|
+
assert env.PORT == 8080
|
|
82
|
+
|
|
83
|
+
def test_overrides_default_when_set(self):
|
|
84
|
+
set_env(PORT="9000")
|
|
85
|
+
|
|
86
|
+
class Env(EnvGuard):
|
|
87
|
+
PORT: int = 8080
|
|
88
|
+
|
|
89
|
+
env = Env()
|
|
90
|
+
assert env.PORT == 9000
|
|
91
|
+
clear_env("PORT")
|
|
92
|
+
|
|
93
|
+
def test_optional_str_default(self):
|
|
94
|
+
clear_env("LOG_LEVEL")
|
|
95
|
+
|
|
96
|
+
class Env(EnvGuard):
|
|
97
|
+
LOG_LEVEL: str = "INFO"
|
|
98
|
+
|
|
99
|
+
env = Env()
|
|
100
|
+
assert env.LOG_LEVEL == "INFO"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestBoolCoercion:
|
|
104
|
+
@pytest.mark.parametrize("value", ["true", "True", "TRUE", "1", "yes", "on"])
|
|
105
|
+
def test_truthy_values(self, value):
|
|
106
|
+
set_env(DEBUG=value)
|
|
107
|
+
|
|
108
|
+
class Env(EnvGuard):
|
|
109
|
+
DEBUG: bool
|
|
110
|
+
|
|
111
|
+
assert Env().DEBUG is True
|
|
112
|
+
clear_env("DEBUG")
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize("value", ["false", "False", "FALSE", "0", "no", "off"])
|
|
115
|
+
def test_falsy_values(self, value):
|
|
116
|
+
set_env(DEBUG=value)
|
|
117
|
+
|
|
118
|
+
class Env(EnvGuard):
|
|
119
|
+
DEBUG: bool
|
|
120
|
+
|
|
121
|
+
assert Env().DEBUG is False
|
|
122
|
+
clear_env("DEBUG")
|
|
123
|
+
|
|
124
|
+
def test_invalid_bool_raises(self):
|
|
125
|
+
set_env(DEBUG="maybe")
|
|
126
|
+
|
|
127
|
+
class Env(EnvGuard):
|
|
128
|
+
DEBUG: bool
|
|
129
|
+
|
|
130
|
+
with pytest.raises(EnvGuardError) as exc:
|
|
131
|
+
Env()
|
|
132
|
+
assert "DEBUG" in str(exc.value)
|
|
133
|
+
clear_env("DEBUG")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestListCoercion:
|
|
137
|
+
def test_comma_separated_list(self):
|
|
138
|
+
set_env(ALLOWED_HOSTS="localhost,example.com,api.example.com")
|
|
139
|
+
|
|
140
|
+
class Env(EnvGuard):
|
|
141
|
+
ALLOWED_HOSTS: list
|
|
142
|
+
|
|
143
|
+
env = Env()
|
|
144
|
+
assert env.ALLOWED_HOSTS == ["localhost", "example.com", "api.example.com"]
|
|
145
|
+
clear_env("ALLOWED_HOSTS")
|
|
146
|
+
|
|
147
|
+
def test_list_strips_whitespace(self):
|
|
148
|
+
set_env(HOSTS=" a , b , c ")
|
|
149
|
+
|
|
150
|
+
class Env(EnvGuard):
|
|
151
|
+
HOSTS: list
|
|
152
|
+
|
|
153
|
+
env = Env()
|
|
154
|
+
assert env.HOSTS == ["a", "b", "c"]
|
|
155
|
+
clear_env("HOSTS")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestInvalidType:
|
|
159
|
+
def test_invalid_int_raises(self):
|
|
160
|
+
set_env(PORT="not-a-number")
|
|
161
|
+
|
|
162
|
+
class Env(EnvGuard):
|
|
163
|
+
PORT: int
|
|
164
|
+
|
|
165
|
+
with pytest.raises(EnvGuardError) as exc:
|
|
166
|
+
Env()
|
|
167
|
+
assert "PORT" in str(exc.value)
|
|
168
|
+
assert "int" in str(exc.value)
|
|
169
|
+
clear_env("PORT")
|
|
170
|
+
|
|
171
|
+
def test_reports_all_invalid_at_once(self):
|
|
172
|
+
set_env(PORT="bad", WORKERS="also-bad")
|
|
173
|
+
|
|
174
|
+
class Env(EnvGuard):
|
|
175
|
+
PORT: int
|
|
176
|
+
WORKERS: int
|
|
177
|
+
|
|
178
|
+
with pytest.raises(EnvGuardError) as exc:
|
|
179
|
+
Env()
|
|
180
|
+
assert "PORT" in str(exc.value)
|
|
181
|
+
assert "WORKERS" in str(exc.value)
|
|
182
|
+
clear_env("PORT", "WORKERS")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# --- guard() one-liner style ---
|
|
186
|
+
|
|
187
|
+
class TestGuard:
|
|
188
|
+
def test_guard_basic(self):
|
|
189
|
+
set_env(HOST="localhost")
|
|
190
|
+
env = guard(HOST=str)
|
|
191
|
+
assert env.HOST == "localhost"
|
|
192
|
+
clear_env("HOST")
|
|
193
|
+
|
|
194
|
+
def test_guard_raises_on_missing(self):
|
|
195
|
+
clear_env("SECRET_KEY")
|
|
196
|
+
with pytest.raises(EnvGuardError):
|
|
197
|
+
guard(SECRET_KEY=str)
|
|
198
|
+
|
|
199
|
+
def test_guard_int(self):
|
|
200
|
+
set_env(TIMEOUT="30")
|
|
201
|
+
env = guard(TIMEOUT=int)
|
|
202
|
+
assert env.TIMEOUT == 30
|
|
203
|
+
clear_env("TIMEOUT")
|