envertor 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.
- envertor-0.1.0/LICENSE +0 -0
- envertor-0.1.0/PKG-INFO +163 -0
- envertor-0.1.0/README.md +146 -0
- envertor-0.1.0/envertor/__init__.py +3 -0
- envertor-0.1.0/envertor/checker.py +64 -0
- envertor-0.1.0/envertor/cli.py +113 -0
- envertor-0.1.0/envertor/core.py +62 -0
- envertor-0.1.0/envertor/gitignore.py +25 -0
- envertor-0.1.0/envertor/scanner.py +74 -0
- envertor-0.1.0/envertor/version.py +2 -0
- envertor-0.1.0/envertor.egg-info/PKG-INFO +163 -0
- envertor-0.1.0/envertor.egg-info/SOURCES.txt +16 -0
- envertor-0.1.0/envertor.egg-info/dependency_links.txt +1 -0
- envertor-0.1.0/envertor.egg-info/entry_points.txt +2 -0
- envertor-0.1.0/envertor.egg-info/top_level.txt +1 -0
- envertor-0.1.0/pyproject.toml +30 -0
- envertor-0.1.0/setup.cfg +4 -0
- envertor-0.1.0/tests/test_basic.py +109 -0
envertor-0.1.0/LICENSE
ADDED
|
File without changes
|
envertor-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: envertor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate example .env files from existing .env files with placeholder detection.
|
|
5
|
+
Author-email: Samin Yeasar <syeasar.kuet@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Y3454R/envertor
|
|
7
|
+
Project-URL: Repository, https://github.com/Y3454R/envertor
|
|
8
|
+
Keywords: dotenv,env,environment,cli,devtools
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Envertor 🔍
|
|
19
|
+
|
|
20
|
+
Envertor is a CLI tool that generates `.env.example` files by extracting environment variables from existing `.env` files or by scanning Python and JavaScript/TypeScript projects. It also helps keep your secrets safe with automatic `.gitignore` protection and CI/CD-ready parity checks.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install envertor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Development install:**
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Generate `.env.example` from an existing `.env`
|
|
38
|
+
|
|
39
|
+
Strips real values and replaces them with type-appropriate placeholders:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
envertor -i .env -o .env.example
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Original `.env` | Generated `.env.example` |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `SECRET=mysecret` | `SECRET=''` |
|
|
48
|
+
| `PORT=8080` | `PORT=0` |
|
|
49
|
+
| `DEBUG=true` | `DEBUG=false` |
|
|
50
|
+
| `RATE=3.14` | `RATE=0.0` |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Scan a project for environment variables
|
|
55
|
+
|
|
56
|
+
Detects env vars used in source code and generates `.env.example` from actual usage — no existing `.env` required:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
envertor -p ./my-project -o .env.example
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Limit scanning to a specific language:
|
|
63
|
+
```bash
|
|
64
|
+
envertor -p ./my-project --lang python
|
|
65
|
+
envertor -p ./my-project --lang js
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Supports:
|
|
69
|
+
- **Python**: `os.getenv("VAR")`, `os.environ["VAR"]`, `os.environ.get("VAR")`
|
|
70
|
+
- **JS/TS**: `process.env.VAR` (`.js`, `.ts`, `.jsx`, `.tsx`)
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### Create `.env` from `.env.example`
|
|
75
|
+
|
|
76
|
+
Bootstrap a local `.env` from the example file so teammates can fill in their own values:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
envertor --create-env # reads .env.example by default
|
|
80
|
+
envertor --create-env staging.env.example # reads a custom file
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If `.env` already exists, writes `.env.envertor` instead to avoid overwriting.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### Check parity between `.env` and `.env.example`
|
|
88
|
+
|
|
89
|
+
Explicitly verify that both files have the same keys. Designed for CI/CD pipelines — exits `1` on mismatch:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
envertor --check
|
|
93
|
+
envertor --check --env-file /deploy/.env --example-file /repo/.env.example
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Example output on failure:
|
|
97
|
+
```
|
|
98
|
+
[envertor] FAIL: Keys mismatch between .env and .env.example
|
|
99
|
+
Missing from .env: NEW_KEY
|
|
100
|
+
Missing from .env.example: OLD_KEY
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use in a pipeline:
|
|
104
|
+
```yaml
|
|
105
|
+
# GitHub Actions example
|
|
106
|
+
- name: Check env parity
|
|
107
|
+
run: envertor --check --env-file .env --example-file .env.example
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### Show version
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
envertor -v
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Automatic safety checks
|
|
121
|
+
|
|
122
|
+
Every time envertor runs, it performs these checks automatically:
|
|
123
|
+
|
|
124
|
+
**`.gitignore` protection** — ensures `.env` is listed in `.gitignore`. Creates the file if it doesn't exist:
|
|
125
|
+
```
|
|
126
|
+
[envertor] Created .gitignore with .env
|
|
127
|
+
[envertor] Added .env to .gitignore
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Key parity warning** — warns if `.env` and `.env.example` have different keys:
|
|
131
|
+
```
|
|
132
|
+
[envertor] WARNING: Keys in .env not documented in .env.example: DB_URL
|
|
133
|
+
[envertor] WARNING: Keys in .env.example not found in .env: NEW_KEY
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Leftover values warning** — warns if `.env.example` contains non-placeholder values (real secrets accidentally left behind):
|
|
137
|
+
```
|
|
138
|
+
[envertor] WARNING: .env.example has a real value set for API_KEY
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Options
|
|
144
|
+
|
|
145
|
+
| Flag | Description |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `-i, --input FILE` | Path to input `.env` file |
|
|
148
|
+
| `-o, --output FILE` | Path to output `.env.example` (default: `.env.example`) |
|
|
149
|
+
| `-p, --project DIR` | Project folder to scan for env variable usage |
|
|
150
|
+
| `--lang python\|js\|both` | Language filter for project scanning (default: `both`) |
|
|
151
|
+
| `--create-env [FILE]` | Create `.env` from `FILE` (default: `.env.example`) |
|
|
152
|
+
| `--check` | Check key parity and exit `1` on mismatch (CI/CD mode) |
|
|
153
|
+
| `--env-file FILE` | `.env` path for `--check` (default: `.env`) |
|
|
154
|
+
| `--example-file FILE` | `.env.example` path for `--check` (default: `.env.example`) |
|
|
155
|
+
| `-v, --version` | Show version |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Notes
|
|
160
|
+
|
|
161
|
+
- Automatically skips `node_modules/`, `venv/`, `__pycache__/`, `.next/`, `.git/`, `.idea/`, `.vscode/`
|
|
162
|
+
- Regex-based scanning catches the most common patterns (`os.getenv`, `os.environ`, `process.env`)
|
|
163
|
+
- `.env.envertor` is a safe backup — rename it to `.env` or diff it against your existing one
|
envertor-0.1.0/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Envertor 🔍
|
|
2
|
+
|
|
3
|
+
Envertor is a CLI tool that generates `.env.example` files by extracting environment variables from existing `.env` files or by scanning Python and JavaScript/TypeScript projects. It also helps keep your secrets safe with automatic `.gitignore` protection and CI/CD-ready parity checks.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install envertor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Development install:**
|
|
12
|
+
```bash
|
|
13
|
+
pip install -e .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Generate `.env.example` from an existing `.env`
|
|
21
|
+
|
|
22
|
+
Strips real values and replaces them with type-appropriate placeholders:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
envertor -i .env -o .env.example
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Original `.env` | Generated `.env.example` |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `SECRET=mysecret` | `SECRET=''` |
|
|
31
|
+
| `PORT=8080` | `PORT=0` |
|
|
32
|
+
| `DEBUG=true` | `DEBUG=false` |
|
|
33
|
+
| `RATE=3.14` | `RATE=0.0` |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### Scan a project for environment variables
|
|
38
|
+
|
|
39
|
+
Detects env vars used in source code and generates `.env.example` from actual usage — no existing `.env` required:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
envertor -p ./my-project -o .env.example
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Limit scanning to a specific language:
|
|
46
|
+
```bash
|
|
47
|
+
envertor -p ./my-project --lang python
|
|
48
|
+
envertor -p ./my-project --lang js
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Supports:
|
|
52
|
+
- **Python**: `os.getenv("VAR")`, `os.environ["VAR"]`, `os.environ.get("VAR")`
|
|
53
|
+
- **JS/TS**: `process.env.VAR` (`.js`, `.ts`, `.jsx`, `.tsx`)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### Create `.env` from `.env.example`
|
|
58
|
+
|
|
59
|
+
Bootstrap a local `.env` from the example file so teammates can fill in their own values:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
envertor --create-env # reads .env.example by default
|
|
63
|
+
envertor --create-env staging.env.example # reads a custom file
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If `.env` already exists, writes `.env.envertor` instead to avoid overwriting.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### Check parity between `.env` and `.env.example`
|
|
71
|
+
|
|
72
|
+
Explicitly verify that both files have the same keys. Designed for CI/CD pipelines — exits `1` on mismatch:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
envertor --check
|
|
76
|
+
envertor --check --env-file /deploy/.env --example-file /repo/.env.example
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Example output on failure:
|
|
80
|
+
```
|
|
81
|
+
[envertor] FAIL: Keys mismatch between .env and .env.example
|
|
82
|
+
Missing from .env: NEW_KEY
|
|
83
|
+
Missing from .env.example: OLD_KEY
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use in a pipeline:
|
|
87
|
+
```yaml
|
|
88
|
+
# GitHub Actions example
|
|
89
|
+
- name: Check env parity
|
|
90
|
+
run: envertor --check --env-file .env --example-file .env.example
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Show version
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
envertor -v
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Automatic safety checks
|
|
104
|
+
|
|
105
|
+
Every time envertor runs, it performs these checks automatically:
|
|
106
|
+
|
|
107
|
+
**`.gitignore` protection** — ensures `.env` is listed in `.gitignore`. Creates the file if it doesn't exist:
|
|
108
|
+
```
|
|
109
|
+
[envertor] Created .gitignore with .env
|
|
110
|
+
[envertor] Added .env to .gitignore
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Key parity warning** — warns if `.env` and `.env.example` have different keys:
|
|
114
|
+
```
|
|
115
|
+
[envertor] WARNING: Keys in .env not documented in .env.example: DB_URL
|
|
116
|
+
[envertor] WARNING: Keys in .env.example not found in .env: NEW_KEY
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Leftover values warning** — warns if `.env.example` contains non-placeholder values (real secrets accidentally left behind):
|
|
120
|
+
```
|
|
121
|
+
[envertor] WARNING: .env.example has a real value set for API_KEY
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Options
|
|
127
|
+
|
|
128
|
+
| Flag | Description |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `-i, --input FILE` | Path to input `.env` file |
|
|
131
|
+
| `-o, --output FILE` | Path to output `.env.example` (default: `.env.example`) |
|
|
132
|
+
| `-p, --project DIR` | Project folder to scan for env variable usage |
|
|
133
|
+
| `--lang python\|js\|both` | Language filter for project scanning (default: `both`) |
|
|
134
|
+
| `--create-env [FILE]` | Create `.env` from `FILE` (default: `.env.example`) |
|
|
135
|
+
| `--check` | Check key parity and exit `1` on mismatch (CI/CD mode) |
|
|
136
|
+
| `--env-file FILE` | `.env` path for `--check` (default: `.env`) |
|
|
137
|
+
| `--example-file FILE` | `.env.example` path for `--check` (default: `.env.example`) |
|
|
138
|
+
| `-v, --version` | Show version |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Notes
|
|
143
|
+
|
|
144
|
+
- Automatically skips `node_modules/`, `venv/`, `__pycache__/`, `.next/`, `.git/`, `.idea/`, `.vscode/`
|
|
145
|
+
- Regex-based scanning catches the most common patterns (`os.getenv`, `os.environ`, `process.env`)
|
|
146
|
+
- `.env.envertor` is a safe backup — rename it to `.env` or diff it against your existing one
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
SAFE_PLACEHOLDERS = {"", "''", '""', "0", "0.0", "false"}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _parse_keys(filepath: str) -> set:
|
|
7
|
+
keys = set()
|
|
8
|
+
with open(filepath, "r") as f:
|
|
9
|
+
for line in f:
|
|
10
|
+
stripped = line.strip()
|
|
11
|
+
if not stripped or stripped.startswith("#"):
|
|
12
|
+
continue
|
|
13
|
+
if "=" in stripped:
|
|
14
|
+
key = stripped.split("=", 1)[0].strip()
|
|
15
|
+
keys.add(key)
|
|
16
|
+
return keys
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_safe_placeholder(value: str) -> bool:
|
|
20
|
+
value = value.strip()
|
|
21
|
+
if "#" in value:
|
|
22
|
+
value = value.split("#", 1)[0].strip()
|
|
23
|
+
return value.lower() in SAFE_PLACEHOLDERS
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_key_parity(env_path: str, example_path: str, strict: bool = False):
|
|
27
|
+
env_keys = _parse_keys(env_path)
|
|
28
|
+
example_keys = _parse_keys(example_path)
|
|
29
|
+
|
|
30
|
+
missing_from_env = example_keys - env_keys
|
|
31
|
+
missing_from_example = env_keys - example_keys
|
|
32
|
+
|
|
33
|
+
if not missing_from_env and not missing_from_example:
|
|
34
|
+
if strict:
|
|
35
|
+
print(f"[envertor] OK: {env_path} and {example_path} keys match.")
|
|
36
|
+
return True
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
if strict:
|
|
40
|
+
print(f"[envertor] FAIL: Keys mismatch between {env_path} and {example_path}")
|
|
41
|
+
if missing_from_env:
|
|
42
|
+
print(f" Missing from {env_path}: {', '.join(sorted(missing_from_env))}")
|
|
43
|
+
if missing_from_example:
|
|
44
|
+
print(f" Missing from {example_path}: {', '.join(sorted(missing_from_example))}")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
if missing_from_env:
|
|
48
|
+
print(f"[envertor] WARNING: Keys in {example_path} not found in {env_path}: {', '.join(sorted(missing_from_env))}")
|
|
49
|
+
if missing_from_example:
|
|
50
|
+
print(f"[envertor] WARNING: Keys in {env_path} not documented in {example_path}: {', '.join(sorted(missing_from_example))}")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_example_values(example_path: str) -> None:
|
|
55
|
+
with open(example_path, "r") as f:
|
|
56
|
+
for line in f:
|
|
57
|
+
stripped = line.strip()
|
|
58
|
+
if not stripped or stripped.startswith("#"):
|
|
59
|
+
continue
|
|
60
|
+
if "=" in stripped:
|
|
61
|
+
key, value = stripped.split("=", 1)
|
|
62
|
+
key = key.strip()
|
|
63
|
+
if not _is_safe_placeholder(value):
|
|
64
|
+
print(f"[envertor] WARNING: {example_path} has a real value set for {key}")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
from .core import generate_example_env
|
|
7
|
+
from .scanner import scan_project
|
|
8
|
+
from .gitignore import ensure_env_in_gitignore
|
|
9
|
+
from .checker import check_key_parity, check_example_values
|
|
10
|
+
from .version import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Envertor: Generate example .env files from existing .env or project"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"-i", "--input",
|
|
20
|
+
default=None,
|
|
21
|
+
help="Path to input .env file"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"-o", "--output",
|
|
25
|
+
default=".env.example",
|
|
26
|
+
help="Path to output example .env file"
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-p", "--project",
|
|
30
|
+
default=None,
|
|
31
|
+
help="Project folder to auto-scan for env variables"
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--lang",
|
|
35
|
+
choices=["python", "js", "both"],
|
|
36
|
+
default="both",
|
|
37
|
+
help="Languages to scan in project (default: both)"
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-v", "--version",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Show envertor version"
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--create-env",
|
|
46
|
+
nargs="?",
|
|
47
|
+
const=".env.example",
|
|
48
|
+
default=None,
|
|
49
|
+
metavar="FILE",
|
|
50
|
+
help="Create .env from FILE (default: .env.example). Writes .env.envertor if .env already exists."
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--check",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="Check that .env and .env.example have matching keys. Exits 1 on mismatch."
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--env-file",
|
|
59
|
+
default=".env",
|
|
60
|
+
help="Path to .env file for --check (default: .env)"
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--example-file",
|
|
64
|
+
default=".env.example",
|
|
65
|
+
help="Path to .env.example file for --check (default: .env.example)"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
args = parser.parse_args()
|
|
69
|
+
|
|
70
|
+
if args.version:
|
|
71
|
+
print(f"envertor v{__version__}")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if args.check:
|
|
75
|
+
ok = check_key_parity(args.env_file, args.example_file, strict=True)
|
|
76
|
+
sys.exit(0 if ok else 1)
|
|
77
|
+
|
|
78
|
+
if args.create_env is not None:
|
|
79
|
+
source = args.create_env
|
|
80
|
+
if not os.path.exists(source):
|
|
81
|
+
print(f"[envertor] ERROR: Source file '{source}' not found.")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
dest = ".env" if not os.path.exists(".env") else ".env.envertor"
|
|
84
|
+
shutil.copy(source, dest)
|
|
85
|
+
print(f"[envertor] Created {dest} from {source}")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if args.project:
|
|
89
|
+
languages = ("python", "js") if args.lang == "both" else (args.lang,)
|
|
90
|
+
env_vars = scan_project(args.project, languages)
|
|
91
|
+
with open(args.output, "w") as f:
|
|
92
|
+
for key in sorted(env_vars):
|
|
93
|
+
f.write(f"{key}=\n")
|
|
94
|
+
print(f"Generated {args.output} from project scan.")
|
|
95
|
+
ensure_env_in_gitignore(args.project)
|
|
96
|
+
env_path = os.path.join(args.project, ".env")
|
|
97
|
+
if os.path.exists(env_path) and os.path.exists(args.output):
|
|
98
|
+
check_key_parity(env_path, args.output)
|
|
99
|
+
if os.path.exists(args.output):
|
|
100
|
+
check_example_values(args.output)
|
|
101
|
+
|
|
102
|
+
elif args.input:
|
|
103
|
+
generate_example_env(args.input, args.output)
|
|
104
|
+
print(f"Created {args.output} from {args.input}")
|
|
105
|
+
project_dir = os.path.dirname(os.path.abspath(args.input))
|
|
106
|
+
ensure_env_in_gitignore(project_dir)
|
|
107
|
+
if os.path.exists(args.input) and os.path.exists(args.output):
|
|
108
|
+
check_key_parity(args.input, args.output)
|
|
109
|
+
if os.path.exists(args.output):
|
|
110
|
+
check_example_values(args.output)
|
|
111
|
+
|
|
112
|
+
else:
|
|
113
|
+
print("Provide either --input or --project")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# envertor/core.py
|
|
2
|
+
|
|
3
|
+
def detect_placeholder(value):
|
|
4
|
+
value = value.strip()
|
|
5
|
+
|
|
6
|
+
if value.lower() in ["true", "false"]:
|
|
7
|
+
return "false"
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
int(value)
|
|
11
|
+
return "0"
|
|
12
|
+
except ValueError:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
float(value)
|
|
17
|
+
return "0.0"
|
|
18
|
+
except ValueError:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
return "''"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_example_env(input_file, output_file):
|
|
25
|
+
with open(input_file, "r") as f:
|
|
26
|
+
lines = f.readlines()
|
|
27
|
+
|
|
28
|
+
new_lines = []
|
|
29
|
+
|
|
30
|
+
for line in lines:
|
|
31
|
+
stripped = line.strip()
|
|
32
|
+
|
|
33
|
+
if stripped.startswith("#") or not stripped:
|
|
34
|
+
new_lines.append(line)
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
if "=" in line:
|
|
38
|
+
key, value_part = line.split("=", 1)
|
|
39
|
+
key = key.strip()
|
|
40
|
+
|
|
41
|
+
if "#" in value_part:
|
|
42
|
+
value, inline_comment = value_part.split("#", 1)
|
|
43
|
+
value = value.strip()
|
|
44
|
+
inline_comment = "# " + inline_comment.strip()
|
|
45
|
+
else:
|
|
46
|
+
value = value_part.strip()
|
|
47
|
+
inline_comment = ""
|
|
48
|
+
|
|
49
|
+
placeholder = detect_placeholder(value)
|
|
50
|
+
|
|
51
|
+
if inline_comment:
|
|
52
|
+
new_line = f"{key}={placeholder} {inline_comment}\n"
|
|
53
|
+
else:
|
|
54
|
+
new_line = f"{key}={placeholder}\n"
|
|
55
|
+
|
|
56
|
+
new_lines.append(new_line)
|
|
57
|
+
else:
|
|
58
|
+
new_lines.append(line)
|
|
59
|
+
|
|
60
|
+
with open(output_file, "w") as f:
|
|
61
|
+
f.writelines(new_lines)
|
|
62
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def ensure_env_in_gitignore(project_path: str) -> None:
|
|
5
|
+
gitignore_path = os.path.join(project_path, ".gitignore")
|
|
6
|
+
|
|
7
|
+
if not os.path.exists(gitignore_path):
|
|
8
|
+
with open(gitignore_path, "w") as f:
|
|
9
|
+
f.write(".env\n")
|
|
10
|
+
print("[envertor] Created .gitignore with .env")
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
with open(gitignore_path, "r") as f:
|
|
14
|
+
lines = f.readlines()
|
|
15
|
+
|
|
16
|
+
for line in lines:
|
|
17
|
+
stripped = line.strip()
|
|
18
|
+
if stripped in (".env", "*.env"):
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
with open(gitignore_path, "a") as f:
|
|
22
|
+
if lines and not lines[-1].endswith("\n"):
|
|
23
|
+
f.write("\n")
|
|
24
|
+
f.write(".env\n")
|
|
25
|
+
print("[envertor] Added .env to .gitignore")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# envertor/scanner.py
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# Directories that should NEVER be scanned
|
|
6
|
+
SKIP_DIRS = {
|
|
7
|
+
"node_modules",
|
|
8
|
+
"venv",
|
|
9
|
+
"__pycache__",
|
|
10
|
+
".next",
|
|
11
|
+
".git",
|
|
12
|
+
".idea",
|
|
13
|
+
".vscode"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Regex patterns
|
|
17
|
+
PY_GETENV_PATTERN = re.compile(r'os\.getenv\(\s*[\'"]([A-Z0-9_]+)[\'"]\s*\)')
|
|
18
|
+
PY_ENVIRON_PATTERN = re.compile(
|
|
19
|
+
r'os\.environ(?:\.get|\[)\s*[\'"]([A-Z0-9_]+)[\'"]\s*[\]\)]'
|
|
20
|
+
)
|
|
21
|
+
JS_ENV_PATTERN = re.compile(r'process\.env\.([A-Z0-9_]+)')
|
|
22
|
+
|
|
23
|
+
def scan_python_file(path: str) -> set[str]:
|
|
24
|
+
"""Extract env vars from a Python file."""
|
|
25
|
+
env_vars = set()
|
|
26
|
+
try:
|
|
27
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
28
|
+
code = f.read()
|
|
29
|
+
|
|
30
|
+
env_vars.update(PY_GETENV_PATTERN.findall(code))
|
|
31
|
+
env_vars.update(PY_ENVIRON_PATTERN.findall(code))
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
return env_vars
|
|
36
|
+
|
|
37
|
+
def scan_js_file(path: str) -> set[str]:
|
|
38
|
+
"""Extract env vars from a JS/TS file."""
|
|
39
|
+
env_vars = set()
|
|
40
|
+
try:
|
|
41
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
42
|
+
code = f.read()
|
|
43
|
+
|
|
44
|
+
env_vars.update(JS_ENV_PATTERN.findall(code))
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
return env_vars
|
|
49
|
+
|
|
50
|
+
def scan_project(project_path: str, languages=("python", "js")) -> set[str]:
|
|
51
|
+
"""
|
|
52
|
+
Scan a project directory for environment variable usage.
|
|
53
|
+
|
|
54
|
+
:param project_path: Root directory of project
|
|
55
|
+
:param languages: ("python", "js") or ("python",) or ("js",)
|
|
56
|
+
:return: Set of environment variable names
|
|
57
|
+
"""
|
|
58
|
+
env_vars = set()
|
|
59
|
+
|
|
60
|
+
for root, dirs, files in os.walk(project_path):
|
|
61
|
+
# 🚫 Skip unwanted directories
|
|
62
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
63
|
+
|
|
64
|
+
for file in files:
|
|
65
|
+
full_path = os.path.join(root, file)
|
|
66
|
+
|
|
67
|
+
if "python" in languages and file.endswith(".py"):
|
|
68
|
+
env_vars.update(scan_python_file(full_path))
|
|
69
|
+
|
|
70
|
+
if "js" in languages and file.endswith((".js", ".ts", ".jsx", ".tsx")):
|
|
71
|
+
env_vars.update(scan_js_file(full_path))
|
|
72
|
+
|
|
73
|
+
return env_vars
|
|
74
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: envertor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate example .env files from existing .env files with placeholder detection.
|
|
5
|
+
Author-email: Samin Yeasar <syeasar.kuet@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Y3454R/envertor
|
|
7
|
+
Project-URL: Repository, https://github.com/Y3454R/envertor
|
|
8
|
+
Keywords: dotenv,env,environment,cli,devtools
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Envertor 🔍
|
|
19
|
+
|
|
20
|
+
Envertor is a CLI tool that generates `.env.example` files by extracting environment variables from existing `.env` files or by scanning Python and JavaScript/TypeScript projects. It also helps keep your secrets safe with automatic `.gitignore` protection and CI/CD-ready parity checks.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install envertor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Development install:**
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Generate `.env.example` from an existing `.env`
|
|
38
|
+
|
|
39
|
+
Strips real values and replaces them with type-appropriate placeholders:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
envertor -i .env -o .env.example
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Original `.env` | Generated `.env.example` |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `SECRET=mysecret` | `SECRET=''` |
|
|
48
|
+
| `PORT=8080` | `PORT=0` |
|
|
49
|
+
| `DEBUG=true` | `DEBUG=false` |
|
|
50
|
+
| `RATE=3.14` | `RATE=0.0` |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Scan a project for environment variables
|
|
55
|
+
|
|
56
|
+
Detects env vars used in source code and generates `.env.example` from actual usage — no existing `.env` required:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
envertor -p ./my-project -o .env.example
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Limit scanning to a specific language:
|
|
63
|
+
```bash
|
|
64
|
+
envertor -p ./my-project --lang python
|
|
65
|
+
envertor -p ./my-project --lang js
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Supports:
|
|
69
|
+
- **Python**: `os.getenv("VAR")`, `os.environ["VAR"]`, `os.environ.get("VAR")`
|
|
70
|
+
- **JS/TS**: `process.env.VAR` (`.js`, `.ts`, `.jsx`, `.tsx`)
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### Create `.env` from `.env.example`
|
|
75
|
+
|
|
76
|
+
Bootstrap a local `.env` from the example file so teammates can fill in their own values:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
envertor --create-env # reads .env.example by default
|
|
80
|
+
envertor --create-env staging.env.example # reads a custom file
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If `.env` already exists, writes `.env.envertor` instead to avoid overwriting.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### Check parity between `.env` and `.env.example`
|
|
88
|
+
|
|
89
|
+
Explicitly verify that both files have the same keys. Designed for CI/CD pipelines — exits `1` on mismatch:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
envertor --check
|
|
93
|
+
envertor --check --env-file /deploy/.env --example-file /repo/.env.example
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Example output on failure:
|
|
97
|
+
```
|
|
98
|
+
[envertor] FAIL: Keys mismatch between .env and .env.example
|
|
99
|
+
Missing from .env: NEW_KEY
|
|
100
|
+
Missing from .env.example: OLD_KEY
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use in a pipeline:
|
|
104
|
+
```yaml
|
|
105
|
+
# GitHub Actions example
|
|
106
|
+
- name: Check env parity
|
|
107
|
+
run: envertor --check --env-file .env --example-file .env.example
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### Show version
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
envertor -v
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Automatic safety checks
|
|
121
|
+
|
|
122
|
+
Every time envertor runs, it performs these checks automatically:
|
|
123
|
+
|
|
124
|
+
**`.gitignore` protection** — ensures `.env` is listed in `.gitignore`. Creates the file if it doesn't exist:
|
|
125
|
+
```
|
|
126
|
+
[envertor] Created .gitignore with .env
|
|
127
|
+
[envertor] Added .env to .gitignore
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Key parity warning** — warns if `.env` and `.env.example` have different keys:
|
|
131
|
+
```
|
|
132
|
+
[envertor] WARNING: Keys in .env not documented in .env.example: DB_URL
|
|
133
|
+
[envertor] WARNING: Keys in .env.example not found in .env: NEW_KEY
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Leftover values warning** — warns if `.env.example` contains non-placeholder values (real secrets accidentally left behind):
|
|
137
|
+
```
|
|
138
|
+
[envertor] WARNING: .env.example has a real value set for API_KEY
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Options
|
|
144
|
+
|
|
145
|
+
| Flag | Description |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `-i, --input FILE` | Path to input `.env` file |
|
|
148
|
+
| `-o, --output FILE` | Path to output `.env.example` (default: `.env.example`) |
|
|
149
|
+
| `-p, --project DIR` | Project folder to scan for env variable usage |
|
|
150
|
+
| `--lang python\|js\|both` | Language filter for project scanning (default: `both`) |
|
|
151
|
+
| `--create-env [FILE]` | Create `.env` from `FILE` (default: `.env.example`) |
|
|
152
|
+
| `--check` | Check key parity and exit `1` on mismatch (CI/CD mode) |
|
|
153
|
+
| `--env-file FILE` | `.env` path for `--check` (default: `.env`) |
|
|
154
|
+
| `--example-file FILE` | `.env.example` path for `--check` (default: `.env.example`) |
|
|
155
|
+
| `-v, --version` | Show version |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Notes
|
|
160
|
+
|
|
161
|
+
- Automatically skips `node_modules/`, `venv/`, `__pycache__/`, `.next/`, `.git/`, `.idea/`, `.vscode/`
|
|
162
|
+
- Regex-based scanning catches the most common patterns (`os.getenv`, `os.environ`, `process.env`)
|
|
163
|
+
- `.env.envertor` is a safe backup — rename it to `.env` or diff it against your existing one
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
envertor/__init__.py
|
|
5
|
+
envertor/checker.py
|
|
6
|
+
envertor/cli.py
|
|
7
|
+
envertor/core.py
|
|
8
|
+
envertor/gitignore.py
|
|
9
|
+
envertor/scanner.py
|
|
10
|
+
envertor/version.py
|
|
11
|
+
envertor.egg-info/PKG-INFO
|
|
12
|
+
envertor.egg-info/SOURCES.txt
|
|
13
|
+
envertor.egg-info/dependency_links.txt
|
|
14
|
+
envertor.egg-info/entry_points.txt
|
|
15
|
+
envertor.egg-info/top_level.txt
|
|
16
|
+
tests/test_basic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
envertor
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "envertor"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Generate example .env files from existing .env files with placeholder detection."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Samin Yeasar", email = "syeasar.kuet@gmail.com" }
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { file = "LICENSE" }
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = []
|
|
12
|
+
keywords = ["dotenv", "env", "environment", "cli", "devtools"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Topic :: Software Development :: Build Tools",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/Y3454R/envertor"
|
|
22
|
+
Repository = "https://github.com/Y3454R/envertor"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
envertor = "envertor.cli:main"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=61.0"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
envertor-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from envertor.core import detect_placeholder
|
|
6
|
+
from envertor.gitignore import ensure_env_in_gitignore
|
|
7
|
+
from envertor.checker import check_key_parity, check_example_values
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_placeholder_detection():
|
|
11
|
+
assert detect_placeholder("123") == "0"
|
|
12
|
+
assert detect_placeholder("3.14") == "0.0"
|
|
13
|
+
assert detect_placeholder("true") == "false"
|
|
14
|
+
assert detect_placeholder("hello") == "''"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- ensure_env_in_gitignore ---
|
|
18
|
+
|
|
19
|
+
def test_gitignore_created_when_missing():
|
|
20
|
+
with tempfile.TemporaryDirectory() as d:
|
|
21
|
+
ensure_env_in_gitignore(d)
|
|
22
|
+
gitignore = os.path.join(d, ".gitignore")
|
|
23
|
+
assert os.path.exists(gitignore)
|
|
24
|
+
assert ".env" in open(gitignore).read()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_gitignore_appended_when_env_missing():
|
|
28
|
+
with tempfile.TemporaryDirectory() as d:
|
|
29
|
+
gitignore = os.path.join(d, ".gitignore")
|
|
30
|
+
with open(gitignore, "w") as f:
|
|
31
|
+
f.write("node_modules/\n")
|
|
32
|
+
ensure_env_in_gitignore(d)
|
|
33
|
+
content = open(gitignore).read()
|
|
34
|
+
assert ".env" in content
|
|
35
|
+
assert "node_modules/" in content
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_gitignore_no_duplicate_when_env_present():
|
|
39
|
+
with tempfile.TemporaryDirectory() as d:
|
|
40
|
+
gitignore = os.path.join(d, ".gitignore")
|
|
41
|
+
with open(gitignore, "w") as f:
|
|
42
|
+
f.write(".env\nnode_modules/\n")
|
|
43
|
+
ensure_env_in_gitignore(d)
|
|
44
|
+
content = open(gitignore).read()
|
|
45
|
+
assert content.count(".env") == 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- check_key_parity ---
|
|
49
|
+
|
|
50
|
+
def _write(path, content):
|
|
51
|
+
with open(path, "w") as f:
|
|
52
|
+
f.write(content)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_parity_match(capsys):
|
|
56
|
+
with tempfile.TemporaryDirectory() as d:
|
|
57
|
+
env = os.path.join(d, ".env")
|
|
58
|
+
example = os.path.join(d, ".env.example")
|
|
59
|
+
_write(env, "KEY_A=secret\nKEY_B=123\n")
|
|
60
|
+
_write(example, "KEY_A=''\nKEY_B=0\n")
|
|
61
|
+
result = check_key_parity(env, example, strict=True)
|
|
62
|
+
assert result is True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_parity_mismatch_strict(capsys):
|
|
66
|
+
with tempfile.TemporaryDirectory() as d:
|
|
67
|
+
env = os.path.join(d, ".env")
|
|
68
|
+
example = os.path.join(d, ".env.example")
|
|
69
|
+
_write(env, "KEY_A=secret\nKEY_C=extra\n")
|
|
70
|
+
_write(example, "KEY_A=''\nKEY_B=''\n")
|
|
71
|
+
result = check_key_parity(env, example, strict=True)
|
|
72
|
+
assert result is False
|
|
73
|
+
out = capsys.readouterr().out
|
|
74
|
+
assert "KEY_B" in out
|
|
75
|
+
assert "KEY_C" in out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_parity_mismatch_warns(capsys):
|
|
79
|
+
with tempfile.TemporaryDirectory() as d:
|
|
80
|
+
env = os.path.join(d, ".env")
|
|
81
|
+
example = os.path.join(d, ".env.example")
|
|
82
|
+
_write(env, "KEY_A=secret\n")
|
|
83
|
+
_write(example, "KEY_A=''\nUNDOCUMENTED=''\n")
|
|
84
|
+
check_key_parity(env, example, strict=False)
|
|
85
|
+
out = capsys.readouterr().out
|
|
86
|
+
assert "WARNING" in out
|
|
87
|
+
assert "UNDOCUMENTED" in out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --- check_example_values ---
|
|
91
|
+
|
|
92
|
+
def test_example_values_safe(capsys):
|
|
93
|
+
with tempfile.TemporaryDirectory() as d:
|
|
94
|
+
example = os.path.join(d, ".env.example")
|
|
95
|
+
_write(example, "KEY_A=''\nKEY_B=0\nKEY_C=false\nKEY_D=\n")
|
|
96
|
+
check_example_values(example)
|
|
97
|
+
out = capsys.readouterr().out
|
|
98
|
+
assert "WARNING" not in out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_example_values_warns_on_real_value(capsys):
|
|
102
|
+
with tempfile.TemporaryDirectory() as d:
|
|
103
|
+
example = os.path.join(d, ".env.example")
|
|
104
|
+
_write(example, "KEY_A=real_secret\nKEY_B=''\n")
|
|
105
|
+
check_example_values(example)
|
|
106
|
+
out = capsys.readouterr().out
|
|
107
|
+
assert "WARNING" in out
|
|
108
|
+
assert "KEY_A" in out
|
|
109
|
+
assert "KEY_B" not in out
|