Skip to content

Commit 28a0c11

Browse files
authored
✨ feat(plugin): add --pytest-env-verbose for debugging env assignments (#199)
When multiple env files, inline config, and CLI options interact it becomes hard to track which values pytest-env actually sets. The new flag prints each action (SET, SKIP, UNSET) with source file in the session header via pytest_report_header, following pytest conventions. Also replaces prettier with mdformat (+ toc/gfm/config plugins) and yamlfmt in pre-commit, and restructures the README for readability while keeping the Diataxis layout.
1 parent ded63b0 commit 28a0c11

File tree

6 files changed

+347
-129
lines changed

6 files changed

+347
-129
lines changed

.github/workflows/check.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ on:
77
pull_request:
88
schedule:
99
- cron: "0 8 * * *"
10-
1110
concurrency:
1211
group: check-${{ github.ref }}
1312
cancel-in-progress: true
14-
1513
jobs:
1614
test:
1715
runs-on: ubuntu-latest

.github/workflows/release.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ name: Release to PyPI
22
on:
33
push:
44
tags: ["*"]
5-
65
env:
76
dists-artifact-name: python-package-distributions
8-
97
jobs:
108
build:
119
runs-on: ubuntu-latest
@@ -26,7 +24,6 @@ jobs:
2624
with:
2725
name: ${{ env.dists-artifact-name }}
2826
path: dist/*
29-
3027
release:
3128
needs:
3229
- build

.pre-commit-config.yaml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,18 @@ repos:
2828
- id: ruff-format
2929
- id: ruff
3030
args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
31-
- repo: https://github.com/rbubley/mirrors-prettier
32-
rev: "v3.8.1"
31+
- repo: https://github.com/hukkin/mdformat
32+
rev: "1.0.0"
3333
hooks:
34-
- id: prettier
34+
- id: mdformat
3535
additional_dependencies:
36-
37-
- "@prettier/[email protected]"
36+
- mdformat-config>=0.2.1
37+
- mdformat-gfm>=1
38+
- mdformat-toc>=0.5
39+
- repo: https://github.com/google/yamlfmt
40+
rev: "v0.21.0"
41+
hooks:
42+
- id: yamlfmt
3843
- repo: meta
3944
hooks:
4045
- id: check-hooks-apply

README.md

Lines changed: 105 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@
88
A `pytest` plugin that sets environment variables from `pyproject.toml`, `pytest.toml`, `.pytest.toml`, or `pytest.ini`
99
configuration files. It can also load variables from `.env` files.
1010

11+
<!-- mdformat-toc start --slug=github --no-anchors --maxlevel=6 --minlevel=2 -->
12+
13+
- [Installation](#installation)
14+
- [Quick start](#quick-start)
15+
- [How-to guides](#how-to-guides)
16+
- [Load variables from `.env` files](#load-variables-from-env-files)
17+
- [Control variable behavior](#control-variable-behavior)
18+
- [Set different environments for test suites](#set-different-environments-for-test-suites)
19+
- [Reference](#reference)
20+
- [TOML configuration format](#toml-configuration-format)
21+
- [INI configuration format](#ini-configuration-format)
22+
- [`.env` file format](#env-file-format)
23+
- [CLI options](#cli-options)
24+
- [`--envfile PATH`](#--envfile-path)
25+
- [`--pytest-env-verbose`](#--pytest-env-verbose)
26+
- [Explanation](#explanation)
27+
- [Precedence](#precedence)
28+
- [File discovery](#file-discovery)
29+
- [Choosing a configuration format](#choosing-a-configuration-format)
30+
31+
<!-- mdformat-toc end -->
32+
1133
## Installation
1234

1335
```shell
@@ -35,36 +57,16 @@ def test_database_connection():
3557
assert os.environ["DEBUG"] == "true"
3658
```
3759

38-
## How-to guides
39-
40-
### Set different environments for test suites
60+
To see exactly what pytest-env sets, pass `--pytest-env-verbose`:
4161

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
5062
```
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
63+
$ pytest --pytest-env-verbose
64+
pytest-env:
65+
SET DATABASE_URL=postgresql://localhost/test_db (from /project/pyproject.toml)
66+
SET DEBUG=true (from /project/pyproject.toml)
6467
```
6568

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.
69+
## How-to guides
6870

6971
### Load variables from `.env` files
7072

@@ -83,48 +85,53 @@ SECRET_KEY='my-secret-key'
8385
DEBUG="true"
8486
```
8587

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:
88+
Files are loaded before inline variables, so inline configuration takes precedence. To switch `.env` files at runtime
89+
without changing configuration, use the `--envfile` CLI option:
9190

92-
```toml
93-
[tool.pytest_env]
94-
RUN_PATH = { value = "/run/path/{USER}", transform = true }
91+
```shell
92+
pytest --envfile .env.local # ignore configured env_files, load only this file
93+
pytest --envfile +.env.override # load configured env_files first, then this file on top
9594
```
9695

97-
The `{USER}` placeholder expands to the current user's name.
96+
### Control variable behavior
9897

99-
### Set conditional defaults
100-
101-
Only set a variable if it does not already exist:
98+
Variables set as plain values are assigned directly. For more control, use inline tables with the `transform`,
99+
`skip_if_set`, and `unset` keys:
102100

103101
```toml
104102
[tool.pytest_env]
103+
SIMPLE = "value"
104+
RUN_PATH = { value = "/run/path/{USER}", transform = true }
105105
HOME = { value = "~/tmp", skip_if_set = true }
106+
TEMP_VAR = { unset = true }
106107
```
107108

108-
This leaves `HOME` unchanged if already set, otherwise sets it to `~/tmp`.
109+
`transform` expands `{VAR}` placeholders using existing environment variables. `skip_if_set` leaves the variable
110+
unchanged when it already exists. `unset` removes it entirely (different from setting to empty string).
109111

110-
### Remove variables from the environment
112+
### Set different environments for test suites
111113

112-
Unset a variable completely (different from setting to empty string):
114+
Create a subdirectory config to override parent settings:
113115

114-
```toml
115-
[tool.pytest_env]
116-
DATABASE_URL = { unset = true }
116+
```
117+
project/
118+
├── pyproject.toml # [tool.pytest_env] DB_HOST = "prod-db"
119+
└── tests_integration/
120+
├── pytest.toml # [pytest_env] DB_HOST = "test-db"
121+
└── test_api.py
117122
```
118123

124+
Running `pytest tests_integration/` uses the subdirectory configuration. The plugin walks up the directory tree and
125+
stops at the first file containing a `pytest_env` section, so subdirectory configs naturally override parent configs.
126+
119127
## Reference
120128

121129
### TOML configuration format
122130

123-
Define environment variables under `[tool.pytest_env]` in `pyproject.toml`, or `[pytest_env]` in `pytest.toml` or
131+
Define environment variables under `[tool.pytest_env]` in `pyproject.toml`, or `[pytest_env]` in `pytest.toml` /
124132
`.pytest.toml`:
125133

126134
```toml
127-
# pyproject.toml
128135
[tool.pytest_env]
129136
SIMPLE_VAR = "value"
130137
NUMBER_VAR = 42
@@ -133,10 +140,8 @@ CONDITIONAL = { value = "default", skip_if_set = true }
133140
REMOVED = { unset = true }
134141
```
135142

136-
Each key is the environment variable name. Values can be:
137-
138-
- **Plain values**: Cast to string and set directly.
139-
- **Inline tables**: Objects with the following keys:
143+
Each key is the environment variable name. Values can be plain values (cast to string) or inline tables with the
144+
following keys:
140145

141146
| Key | Type | Description |
142147
| ------------- | ------ | ---------------------------------------------------------------------------- |
@@ -171,13 +176,13 @@ env = [
171176

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

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. |
179+
| Flag | Description |
180+
| ---- | -------------------------------------------------------------------- |
181+
| `D:` | Default -- only set if the variable is not already defined. |
182+
| `R:` | Raw -- skip `{VAR}` expansion (INI expands by default, unlike TOML). |
183+
| `U:` | Unset -- remove the variable from the environment entirely. |
179184

180-
**Note**: In INI format, variable expansion is enabled by default. In TOML format, it requires `transform = true`.
185+
In INI format variable expansion is enabled by default. In TOML format it requires `transform = true`.
181186

182187
### `.env` file format
183188

@@ -195,13 +200,8 @@ env_files =
195200
.env.test
196201
```
197202

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
203+
Files are parsed by [python-dotenv](https://github.com/theskumar/python-dotenv) and support `KEY=VALUE` lines, `#`
204+
comments, `export` prefix, quoted values with escape sequences in double quotes, and `${VAR:-default}` expansion.
205205

206206
Example `.env` file:
207207

@@ -213,83 +213,71 @@ MESSAGE="hello\nworld"
213213
API_KEY=${FALLBACK_KEY:-default_key}
214214
```
215215

216-
Missing `.env` files are silently skipped. Paths are resolved relative to the project root.
216+
Missing `.env` files from configuration are silently skipped. Paths are resolved relative to the project root.
217217

218-
### CLI option: `--envfile`
218+
### CLI options
219219

220-
Override or extend configuration-based `env_files` at runtime:
220+
#### `--envfile PATH`
221221

222-
```shell
223-
pytest --envfile PATH # Override mode
224-
pytest --envfile +PATH # Extend mode
225-
```
222+
Override or extend configuration-based `env_files` at runtime.
226223

227-
**Override mode** (`--envfile PATH`): Loads only the specified file, ignoring all `env_files` from configuration.
224+
**Override mode** (`--envfile PATH`): loads only the specified file, ignoring all `env_files` from configuration.
228225

229-
**Extend mode** (`--envfile +PATH`): Loads configuration files first in their normal order, then loads the CLI file.
226+
**Extend mode** (`--envfile +PATH`): loads configuration files first in their normal order, then loads the CLI file.
230227
Variables from the CLI file override those from configuration files.
231228

232229
Unlike configuration-based `env_files`, CLI-specified files must exist. Missing files raise `FileNotFoundError`. Paths
233230
are resolved relative to the project root.
234231

235-
## Explanation
232+
#### `--pytest-env-verbose`
236233

237-
### Configuration precedence
234+
Print all environment variable assignments in the test session header. Each line shows the action (`SET`, `SKIP`, or
235+
`UNSET`), the variable name with its final value, and the source file:
238236

239-
When multiple configuration sources define the same variable, the following precedence rules apply (highest to lowest):
237+
```
238+
pytest-env:
239+
SET DATABASE_URL=postgres://localhost/test (from /path/to/.env)
240+
SET DEBUG=true (from /path/to/pyproject.toml)
241+
SKIP HOME=/Users/me (from /path/to/pyproject.toml)
242+
UNSET TEMP_VAR (from /path/to/pyproject.toml)
243+
```
240244

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)
245+
Useful for debugging when multiple env files, inline configuration, and CLI options interact.
244246

245-
When using `--envfile`, CLI files take precedence over configuration-based `env_files`, but inline variables still win.
247+
## Explanation
246248

247-
### Configuration format precedence
249+
### Precedence
248250

249-
When multiple configuration formats are present:
251+
When multiple sources define the same variable, precedence applies in this order (highest to lowest):
250252

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.
253+
1. Inline variables in configuration files (TOML or INI format).
254+
1. Variables from `.env` files loaded via `env_files`. When using `--envfile`, CLI files take precedence over
255+
configuration-based `env_files`.
256+
1. Variables already present in the environment (preserved when `skip_if_set = true` or `D:` flag is used).
257+
258+
When multiple configuration formats are present, TOML native format (`[pytest_env]` / `[tool.pytest_env]`) takes
259+
precedence over INI format. Among TOML files, the first file with a `pytest_env` section wins, checked in order:
260+
`pytest.toml`, `.pytest.toml`, `pyproject.toml`. If no TOML file contains `pytest_env`, the plugin falls back to
261+
INI-style `env` configuration.
255262

256263
### File discovery
257264

258265
The plugin walks up the directory tree starting from pytest's resolved configuration directory. For each directory, it
259266
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:
267+
`pytest_env` section. This means subdirectory configurations take precedence over parent configurations, allowing
268+
different settings for integration tests versus unit tests.
284269

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).
270+
### Choosing a configuration format
288271

289-
Use **inline configuration** when:
272+
**TOML native format** (`[pytest_env]`) is best when you need fine-grained control over expansion and conditional
273+
setting, or when your configuration uses multiple inline tables. Variable expansion requires explicit
274+
`transform = true`.
290275

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.
276+
**INI format** (`env` key) is best for simple `KEY=VALUE` pairs with minimal syntax. Variable expansion is on by default
277+
(use `R:` to disable). Both formats are fully supported and can coexist -- TOML takes precedence if both are present.
294278

295-
You can combine both approaches. Inline variables always take precedence over `.env` files.
279+
**`.env` files** work well when you have many variables that would clutter your config file, want to share environment
280+
configuration with other tools (Docker, shell scripts), or need different files for different environments. **Inline
281+
configuration** is better for a small number of test-specific variables that should be version-controlled, or when you
282+
need `transform`, `skip_if_set`, or `unset`. You can combine both -- inline variables always take precedence over `.env`
283+
files.

0 commit comments

Comments
 (0)