Skip to content

Commit b723d35

Browse files
committed
✨ feat(cli): add --envfile option for runtime environment switching
Users needed a way to switch between different environment configurations without modifying config files. This is particularly useful when testing against different environments (dev, staging, prod) or when developers need local overrides without changing version-controlled configuration. Implemented a --envfile CLI argument with two modes. Override mode (--envfile PATH) replaces all configured env_files, while extend mode (--envfile +PATH) adds to them. The CLI file always loads last in extend mode, ensuring its variables take precedence. Unlike config-based env_files which silently skip missing files, CLI files must exist to fail fast on typos or incorrect paths. Restructured README following Diátaxis framework to separate tutorial content (quick start), how-to guides (task-focused), reference (complete API), and explanation (conceptual understanding). This makes the documentation more navigable and helps users find what they need based on their goal.
1 parent 973033e commit b723d35

File tree

3 files changed

+335
-121
lines changed

3 files changed

+335
-121
lines changed

README.md

Lines changed: 206 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -14,57 +14,154 @@ configuration files. It can also load variables from `.env` files.
1414
pip install pytest-env
1515
```
1616

17-
## Usage
17+
## Quick start
1818

19-
### TOML configuration (native form)
19+
Add environment variables to your `pyproject.toml`:
2020

21-
Define environment variables under `[tool.pytest_env]` in `pyproject.toml`, or `[pytest_env]` in `pytest.toml` /
22-
`.pytest.toml`:
21+
```toml
22+
[tool.pytest_env]
23+
DATABASE_URL = "postgresql://localhost/test_db"
24+
DEBUG = "true"
25+
```
26+
27+
Run your tests. The environment variables are now available:
28+
29+
```python
30+
import os
31+
32+
33+
def test_database_connection():
34+
assert os.environ["DATABASE_URL"] == "postgresql://localhost/test_db"
35+
assert os.environ["DEBUG"] == "true"
36+
```
37+
38+
## How-to guides
39+
40+
### Set different environments for test suites
41+
42+
Create a subdirectory config to override parent settings:
43+
44+
```
45+
project/
46+
├── pyproject.toml # [tool.pytest_env] DB_HOST = "prod-db"
47+
└── tests_integration/
48+
├── pytest.toml # [pytest_env] DB_HOST = "test-db"
49+
└── test_api.py
50+
```
51+
52+
Running `pytest tests_integration/` uses the subdirectory configuration.
53+
54+
### Switch environments at runtime
55+
56+
Use the `--envfile` CLI option to override or extend your configuration:
57+
58+
```shell
59+
# Override all configured env files with a different one.
60+
pytest --envfile .env.local
61+
62+
# Add an additional env file to those already configured.
63+
pytest --envfile +.env.override
64+
```
65+
66+
Override mode loads only the specified file. Extend mode (prefix with `+`) loads configuration files first, then the CLI
67+
file. Variables in the CLI file take precedence.
68+
69+
### Load variables from `.env` files
70+
71+
Specify `.env` files in your configuration:
2372

2473
```toml
25-
# pyproject.toml
2674
[tool.pytest_env]
27-
HOME = "~/tmp"
28-
RUN_ENV = 1
29-
TRANSFORMED = { value = "{USER}/alpha", transform = true }
30-
SKIP_IF_SET = { value = "on", skip_if_set = true }
31-
DATABASE_URL = { unset = true }
75+
env_files = [".env", ".env.test"]
76+
```
77+
78+
Create your `.env` file:
79+
80+
```shell
81+
DATABASE_URL=postgres://localhost/mydb
82+
SECRET_KEY='my-secret-key'
83+
DEBUG="true"
84+
```
85+
86+
Files are loaded before inline variables, so inline configuration takes precedence.
87+
88+
### Expand variables using other environment variables
89+
90+
Reference existing environment variables in values:
91+
92+
```toml
93+
[tool.pytest_env]
94+
RUN_PATH = { value = "/run/path/{USER}", transform = true }
95+
```
96+
97+
The `{USER}` placeholder expands to the current user's name.
98+
99+
### Set conditional defaults
100+
101+
Only set a variable if it does not already exist:
102+
103+
```toml
104+
[tool.pytest_env]
105+
HOME = { value = "~/tmp", skip_if_set = true }
32106
```
33107

108+
This leaves `HOME` unchanged if already set, otherwise sets it to `~/tmp`.
109+
110+
### Remove variables from the environment
111+
112+
Unset a variable completely (different from setting to empty string):
113+
34114
```toml
35-
# pytest.toml or .pytest.toml
36-
[pytest_env]
37-
HOME = "~/tmp"
38-
RUN_ENV = 1
39-
TRANSFORMED = { value = "{USER}/alpha", transform = true }
40-
SKIP_IF_SET = { value = "on", skip_if_set = true }
115+
[tool.pytest_env]
41116
DATABASE_URL = { unset = true }
42117
```
43118

44-
Each key is the environment variable name. The value is either a plain value (cast to string) or an inline table with
45-
the following keys:
119+
## Reference
120+
121+
### TOML configuration format
122+
123+
Define environment variables under `[tool.pytest_env]` in `pyproject.toml`, or `[pytest_env]` in `pytest.toml` or
124+
`.pytest.toml`:
125+
126+
```toml
127+
# pyproject.toml
128+
[tool.pytest_env]
129+
SIMPLE_VAR = "value"
130+
NUMBER_VAR = 42
131+
EXPANDED = { value = "{HOME}/path", transform = true }
132+
CONDITIONAL = { value = "default", skip_if_set = true }
133+
REMOVED = { unset = true }
134+
```
135+
136+
Each key is the environment variable name. Values can be:
46137

47-
| Key | Type | Description |
48-
| ------------- | ------ | --------------------------------------------------------------------------- |
49-
| `value` | string | The value to set |
50-
| `transform` | bool | Expand `{VAR}` references in the value using existing environment variables |
51-
| `skip_if_set` | bool | Only set the variable if it is not already defined |
52-
| `unset` | bool | Remove the variable from the environment (ignores `value`) |
138+
- **Plain values**: Cast to string and set directly.
139+
- **Inline tables**: Objects with the following keys:
53140

54-
### INI configuration
141+
| Key | Type | Description |
142+
| ------------- | ------ | ---------------------------------------------------------------------------- |
143+
| `value` | string | The value to set. |
144+
| `transform` | bool | Expand `{VAR}` references in the value using existing environment variables. |
145+
| `skip_if_set` | bool | Only set the variable if it is not already defined. |
146+
| `unset` | bool | Remove the variable from the environment (ignores `value`). |
55147

56-
Define environment variables as a line-separated list of `KEY=VALUE` entries under the `env` key:
148+
### INI configuration format
149+
150+
Define environment variables as line-separated `KEY=VALUE` entries:
57151

58152
```ini
59153
# pytest.ini
60154
[pytest]
61155
env =
62156
HOME=~/tmp
63157
RUN_ENV=test
158+
D:CONDITIONAL=value
159+
R:RAW_VALUE={USER}
160+
U:REMOVED_VAR
64161
```
65162

66163
```toml
67-
# pyproject.toml
164+
# pyproject.toml (INI-style)
68165
[tool.pytest]
69166
env = [
70167
"HOME=~/tmp",
@@ -74,133 +171,125 @@ env = [
74171

75172
Prefix flags modify behavior. Flags are case-insensitive and can be combined in any order (e.g., `R:D:KEY=VALUE`):
76173

77-
| Flag | Description |
78-
| ---- | ------------------------------------------------------------------ |
79-
| `D:` | Default — only set if the variable is not already defined |
80-
| `R:` | Raw — skip `{VAR}` expansion (INI expands by default, unlike TOML) |
81-
| `U:` | Unset — remove the variable from the environment entirely |
82-
83-
### Precedence
174+
| Flag | Description |
175+
| ---- | ------------------------------------------------------------------- |
176+
| `D:` | Default — only set if the variable is not already defined. |
177+
| `R:` | Raw — skip `{VAR}` expansion (INI expands by default, unlike TOML). |
178+
| `U:` | Unset — remove the variable from the environment entirely. |
84179

85-
When multiple configuration sources are present, the native TOML form takes precedence over the INI form. Within the
86-
TOML form, files are checked in this order: `pytest.toml`, `.pytest.toml`, `pyproject.toml`. The first file containing a
87-
`pytest_env` section wins.
180+
**Note**: In INI format, variable expansion is enabled by default. In TOML format, it requires `transform = true`.
88181

89-
### Configuration file discovery
182+
### `.env` file format
90183

91-
The plugin walks the directory tree starting from the directory containing the configuration file pytest resolved
92-
(`inipath`). For each directory it checks `pytest.toml`, `.pytest.toml`, and `pyproject.toml` in order, stopping at the
93-
first file with a `pytest_env` section. This means a subdirectory config takes precedence over a parent config:
94-
95-
```
96-
project/
97-
├── pyproject.toml # [tool.pytest_env] DB_HOST = "prod-db"
98-
└── tests_integration/
99-
├── pytest.toml # [pytest_env] DB_HOST = "test-db"
100-
└── test_api.py
101-
```
102-
103-
Running `pytest tests_integration/` uses `DB_HOST = "test-db"` from the subdirectory.
104-
105-
If no TOML file with a `pytest_env` section is found, the plugin falls back to the INI-style `env` key.
106-
107-
### Loading `.env` files
108-
109-
Use `env_files` to load variables from `.env` files. Files are loaded before inline `env` entries, so inline config
110-
takes precedence. Missing files are silently skipped. Paths are relative to the project root.
184+
Specify `.env` files using the `env_files` configuration option:
111185

112186
```toml
113-
# pyproject.toml
114187
[tool.pytest_env]
115188
env_files = [".env", ".env.test"]
116-
API_KEY = "override_value"
117-
```
118-
119-
```toml
120-
# pytest.toml or .pytest.toml
121-
[pytest_env]
122-
env_files = [".env"]
123189
```
124190

125191
```ini
126-
# pytest.ini
127192
[pytest]
128193
env_files =
129194
.env
130195
.env.test
131196
```
132197

133-
Files are parsed by [python-dotenv](https://github.com/theskumar/python-dotenv), supporting `KEY=VALUE` lines, `#`
134-
comments, `export` prefix, quoted values (with escape sequences in double quotes), and `${VAR:-default}` expansion:
198+
Files are parsed by [python-dotenv](https://github.com/theskumar/python-dotenv) and support:
199+
200+
- `KEY=VALUE` lines
201+
- `#` comments
202+
- `export` prefix
203+
- Quoted values with escape sequences in double quotes
204+
- `${VAR:-default}` expansion
205+
206+
Example `.env` file:
135207

136208
```shell
137-
# .env
138209
DATABASE_URL=postgres://localhost/mydb
139210
export SECRET_KEY='my-secret-key'
140211
DEBUG="true"
141212
MESSAGE="hello\nworld"
213+
API_KEY=${FALLBACK_KEY:-default_key}
142214
```
143215

144-
### Examples
216+
Missing `.env` files are silently skipped. Paths are resolved relative to the project root.
145217

146-
**Expanding environment variables** — reference existing variables using `{VAR}` syntax:
218+
### CLI option: `--envfile`
147219

148-
```toml
149-
[pytest_env]
150-
RUN_PATH = { value = "/run/path/{USER}", transform = true }
151-
```
220+
Override or extend configuration-based `env_files` at runtime:
152221

153-
```ini
154-
[pytest]
155-
env =
156-
RUN_PATH=/run/path/{USER}
222+
```shell
223+
pytest --envfile PATH # Override mode
224+
pytest --envfile +PATH # Extend mode
157225
```
158226

159-
In TOML, expansion requires `transform = true`. In INI, expansion is the default; use the `R:` flag to disable it.
227+
**Override mode** (`--envfile PATH`): Loads only the specified file, ignoring all `env_files` from configuration.
160228

161-
**Keeping raw values** — prevent `{VAR}` expansion:
229+
**Extend mode** (`--envfile +PATH`): Loads configuration files first in their normal order, then loads the CLI file.
230+
Variables from the CLI file override those from configuration files.
162231

163-
```toml
164-
[pytest_env]
165-
PATTERN = { value = "/run/path/{USER}" }
166-
```
232+
Unlike configuration-based `env_files`, CLI-specified files must exist. Missing files raise `FileNotFoundError`. Paths
233+
are resolved relative to the project root.
167234

168-
```ini
169-
[pytest]
170-
env =
171-
R:PATTERN=/run/path/{USER}
172-
```
235+
## Explanation
173236

174-
**Conditional defaults** — only set when not already defined:
237+
### Configuration precedence
175238

176-
```toml
177-
[pytest_env]
178-
HOME = { value = "~/tmp", skip_if_set = true }
179-
```
239+
When multiple configuration sources define the same variable, the following precedence rules apply (highest to lowest):
180240

181-
```ini
182-
[pytest]
183-
env =
184-
D:HOME=~/tmp
185-
```
241+
1. Inline variables in configuration files (TOML or INI format)
242+
1. Variables from `.env` files loaded via `env_files`
243+
1. Variables already present in the environment (unless `skip_if_set = false` or no `D:` flag)
186244

187-
**Unsetting variables** — completely remove a variable from `os.environ` (not the same as setting to empty string):
245+
When using `--envfile`, CLI files take precedence over configuration-based `env_files`, but inline variables still win.
188246

189-
```toml
190-
[pytest_env]
191-
DATABASE_URL = { unset = true }
192-
```
247+
### Configuration format precedence
193248

194-
```ini
195-
[pytest]
196-
env =
197-
U:DATABASE_URL
198-
```
249+
When multiple configuration formats are present:
199250

200-
**Combining flags** — flags can be combined in any order:
251+
1. TOML native format (`[pytest_env]` or `[tool.pytest_env]`) takes precedence over INI format.
252+
1. Among TOML files, the first file with a `pytest_env` section is used, checked in order: `pytest.toml`,
253+
`.pytest.toml`, `pyproject.toml`.
254+
1. If no TOML file contains `pytest_env`, the plugin falls back to INI-style `env` configuration.
201255

202-
```ini
203-
[pytest]
204-
env =
205-
R:D:TEMPLATE=/path/{placeholder}
206-
```
256+
### File discovery
257+
258+
The plugin walks up the directory tree starting from pytest's resolved configuration directory. For each directory, it
259+
checks `pytest.toml`, `.pytest.toml`, and `pyproject.toml` in order, stopping at the first file containing a
260+
`pytest_env` section.
261+
262+
This means subdirectory configurations take precedence over parent configurations, allowing you to have different
263+
settings for integration tests versus unit tests.
264+
265+
### When to use TOML vs INI format
266+
267+
Use the **TOML native format** (`[pytest_env]`) when:
268+
269+
- You need fine-grained control over expansion and conditional setting.
270+
- Your configuration is complex with multiple inline tables.
271+
- You prefer explicit `transform = true` for variable expansion.
272+
273+
Use the **INI format** (`env` key) when:
274+
275+
- You want simple `KEY=VALUE` pairs with minimal syntax.
276+
- You prefer expansion by default (add `R:` to disable).
277+
- You are migrating from an existing INI-based setup.
278+
279+
Both formats are fully supported and can coexist (TOML takes precedence if both are present).
280+
281+
### When to use `.env` files vs inline configuration
282+
283+
Use **`.env` files** when:
284+
285+
- You have many environment variables that would clutter your config file.
286+
- You want to share environment configuration with other tools (e.g., Docker, shell scripts).
287+
- You need different `.env` files for different environments (dev, staging, prod).
288+
289+
Use **inline configuration** when:
290+
291+
- You have a small number of test-specific variables.
292+
- You want variables to be version-controlled alongside test configuration.
293+
- You need features like `transform`, `skip_if_set`, or `unset` that `.env` files do not support.
294+
295+
You can combine both approaches. Inline variables always take precedence over `.env` files.

0 commit comments

Comments
 (0)