Skip to content

Commit 7562eed

Browse files
committed
✨ feat(plugin): add --pytest-env-verbose for debugging env assignments
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 7562eed

File tree

6 files changed

+341
-132
lines changed

6 files changed

+341
-132
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-
- prettier@3.8.1
37-
- "@prettier/plugin-xml@3.4.2"
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: 99 additions & 120 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
@@ -37,35 +59,6 @@ def test_database_connection():
3759

3860
## How-to guides
3961

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-
6962
### Load variables from `.env` files
7063

7164
Specify `.env` files in your configuration:
@@ -83,48 +76,53 @@ SECRET_KEY='my-secret-key'
8376
DEBUG="true"
8477
```
8578

86-
Files are loaded before inline variables, so inline configuration takes precedence.
87-
88-
### Expand variables using other environment variables
79+
Files are loaded before inline variables, so inline configuration takes precedence. To switch `.env` files at runtime
80+
without changing configuration, use the `--envfile` CLI option:
8981

90-
Reference existing environment variables in values:
91-
92-
```toml
93-
[tool.pytest_env]
94-
RUN_PATH = { value = "/run/path/{USER}", transform = true }
82+
```shell
83+
pytest --envfile .env.local # ignore configured env_files, load only this file
84+
pytest --envfile +.env.override # load configured env_files first, then this file on top
9585
```
9686

97-
The `{USER}` placeholder expands to the current user's name.
98-
99-
### Set conditional defaults
87+
### Control variable behavior
10088

101-
Only set a variable if it does not already exist:
89+
Variables set as plain values are assigned directly. For more control, use inline tables with the `transform`,
90+
`skip_if_set`, and `unset` keys:
10291

10392
```toml
10493
[tool.pytest_env]
94+
SIMPLE = "value"
95+
RUN_PATH = { value = "/run/path/{USER}", transform = true }
10596
HOME = { value = "~/tmp", skip_if_set = true }
97+
TEMP_VAR = { unset = true }
10698
```
10799

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

110-
### Remove variables from the environment
103+
### Set different environments for test suites
111104

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

114-
```toml
115-
[tool.pytest_env]
116-
DATABASE_URL = { unset = true }
107+
```
108+
project/
109+
├── pyproject.toml # [tool.pytest_env] DB_HOST = "prod-db"
110+
└── tests_integration/
111+
├── pytest.toml # [pytest_env] DB_HOST = "test-db"
112+
└── test_api.py
117113
```
118114

115+
Running `pytest tests_integration/` uses the subdirectory configuration. The plugin walks up the directory tree and
116+
stops at the first file containing a `pytest_env` section, so subdirectory configs naturally override parent configs.
117+
119118
## Reference
120119

121120
### TOML configuration format
122121

123-
Define environment variables under `[tool.pytest_env]` in `pyproject.toml`, or `[pytest_env]` in `pytest.toml` or
122+
Define environment variables under `[tool.pytest_env]` in `pyproject.toml`, or `[pytest_env]` in `pytest.toml` /
124123
`.pytest.toml`:
125124

126125
```toml
127-
# pyproject.toml
128126
[tool.pytest_env]
129127
SIMPLE_VAR = "value"
130128
NUMBER_VAR = 42
@@ -133,10 +131,8 @@ CONDITIONAL = { value = "default", skip_if_set = true }
133131
REMOVED = { unset = true }
134132
```
135133

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:
134+
Each key is the environment variable name. Values can be plain values (cast to string) or inline tables with the
135+
following keys:
140136

141137
| Key | Type | Description |
142138
| ------------- | ------ | ---------------------------------------------------------------------------- |
@@ -171,13 +167,13 @@ env = [
171167

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

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. |
170+
| Flag | Description |
171+
| ---- | -------------------------------------------------------------------- |
172+
| `D:` | Default -- only set if the variable is not already defined. |
173+
| `R:` | Raw -- skip `{VAR}` expansion (INI expands by default, unlike TOML). |
174+
| `U:` | Unset -- remove the variable from the environment entirely. |
179175

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

182178
### `.env` file format
183179

@@ -195,13 +191,8 @@ env_files =
195191
.env.test
196192
```
197193

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

206197
Example `.env` file:
207198

@@ -213,83 +204,71 @@ MESSAGE="hello\nworld"
213204
API_KEY=${FALLBACK_KEY:-default_key}
214205
```
215206

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

218-
### CLI option: `--envfile`
209+
### CLI options
219210

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

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

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

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

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

235-
## Explanation
223+
#### `--pytest-env-verbose`
236224

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

239-
When multiple configuration sources define the same variable, the following precedence rules apply (highest to lowest):
228+
```
229+
pytest-env:
230+
SET DATABASE_URL=postgres://localhost/test (from /path/to/.env)
231+
SET DEBUG=true (from /path/to/pyproject.toml)
232+
SKIP HOME=/Users/me (from /path/to/pyproject.toml)
233+
UNSET TEMP_VAR (from /path/to/pyproject.toml)
234+
```
240235

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

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

247-
### Configuration format precedence
240+
### Precedence
248241

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

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

256254
### File discovery
257255

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

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

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

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

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

0 commit comments

Comments
 (0)