Skip to content

Commit 27896dd

Browse files
authored
Support pytest.toml and .pytest.toml (#180)
1 parent 1349eaa commit 27896dd

File tree

3 files changed

+144
-43
lines changed

3 files changed

+144
-43
lines changed

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ versions](https://img.shields.io/pypi/pyversions/pytest-env.svg)](https://pypi.o
66
[![check](https://github.com/pytest-dev/pytest-env/actions/workflows/check.yaml/badge.svg)](https://github.com/pytest-dev/pytest-env/actions/workflows/check.yaml)
77
[![Downloads](https://static.pepy.tech/badge/pytest-env/month)](https://pepy.tech/project/pytest-env)
88

9-
This is a `pytest` plugin that enables you to set environment variables in a `pytest.ini` or `pyproject.toml` file.
9+
This is a `pytest` plugin that enables you to set environment variables in `pytest.ini`, `pyproject.toml`, `pytest.toml` or `.pytest.toml` files.
1010

1111
## Installation
1212

@@ -18,7 +18,14 @@ pip install pytest-env
1818

1919
## Usage
2020

21-
### Native form in `pyproject.toml`
21+
### Native form in `pyproject.toml`, `pytest.toml` and `.pytest.toml`
22+
23+
> [!NOTE]
24+
> `pytest.toml` and `.pytest.toml` is only supported on Pytest 9.0+.
25+
26+
Native form takes precedence over the `pytest.ini` form. `pytest.toml` takes precedence over `.pytest.toml`, and that takes precedence over `pyproject.toml`.
27+
28+
In `pyproject.toml`:
2229

2330
```toml
2431
[tool.pytest_env]
@@ -28,7 +35,17 @@ TRANSFORMED = {value = "{USER}/alpha", transform = true}
2835
SKIP_IF_SET = {value = "on", skip_if_set = true}
2936
```
3037

31-
The `tool.pytest_env` tables keys are the environment variables keys to set. The right hand side of the assignment:
38+
In `pytest.toml` (or `.pytest.toml`):
39+
40+
```toml
41+
[pytest_env]
42+
HOME = "~/tmp"
43+
RUN_ENV = 1
44+
TRANSFORMED = {value = "{USER}/alpha", transform = true}
45+
SKIP_IF_SET = {value = "on", skip_if_set = true}
46+
```
47+
48+
The `tool.pytest_env` (`pytest_env` in `pytest.toml` and `.pytest.toml`) tables keys are the environment variables keys to set. The right hand side of the assignment:
3249

3350
- if an inline table you can set options via the `transform` or `skip_if_set` keys, while the `value` key holds the
3451
value to set (or transform before setting). For transformation the variables you can use is other environment

src/pytest_env/plugin.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import sys
77
from dataclasses import dataclass
88
from itertools import chain
9-
from typing import TYPE_CHECKING
9+
from typing import TYPE_CHECKING, Any
1010

1111
import pytest
1212

1313
if TYPE_CHECKING:
14-
from collections.abc import Iterator
14+
from collections.abc import Generator, Iterator
1515

1616
if sys.version_info >= (3, 11): # pragma: >=3.11 cover
1717
import tomllib
@@ -49,34 +49,48 @@ def pytest_load_initial_conftests(
4949
os.environ[entry.key] = entry.value.format(**os.environ) if entry.transform else entry.value
5050

5151

52+
def _parse_toml_config(config: dict[str, Any]) -> Generator[Entry, None, None]:
53+
for key, entry in config.items():
54+
if isinstance(entry, dict):
55+
value = str(entry["value"])
56+
transform, skip_if_set = bool(entry.get("transform")), bool(entry.get("skip_if_set"))
57+
else:
58+
value, transform, skip_if_set = str(entry), False, False
59+
yield Entry(key, value, transform, skip_if_set)
60+
61+
5262
def _load_values(early_config: pytest.Config) -> Iterator[Entry]:
53-
has_toml_conf = False
54-
for path in chain.from_iterable([[early_config.rootpath], early_config.rootpath.parents]): # noqa: PLR1702
55-
toml_file = path / "pyproject.toml"
56-
if toml_file.exists():
57-
with toml_file.open("rb") as file_handler:
58-
config = tomllib.load(file_handler)
59-
if "tool" in config and "pytest_env" in config["tool"]:
60-
has_toml_conf = True
61-
for key, entry in config["tool"]["pytest_env"].items():
62-
if isinstance(entry, dict):
63-
value = str(entry["value"])
64-
transform, skip_if_set = bool(entry.get("transform")), bool(entry.get("skip_if_set"))
65-
else:
66-
value, transform, skip_if_set = str(entry), False, False
67-
yield Entry(key, value, transform, skip_if_set)
63+
has_toml = False
64+
for path in chain.from_iterable([[early_config.rootpath], early_config.rootpath.parents]):
65+
for pytest_toml_name in ("pytest.toml", ".pytest.toml", "pyproject.toml"):
66+
pytest_toml_file = path / pytest_toml_name
67+
if pytest_toml_file.exists():
68+
with pytest_toml_file.open("rb") as file_handler:
69+
config = tomllib.load(file_handler)
70+
71+
if pytest_toml_name == "pyproject.toml": # in pyproject.toml the path is tool.pytest_env
72+
config = config.get("tool", {})
73+
74+
if "pytest_env" in config:
75+
has_toml = True
76+
yield from _parse_toml_config(config["pytest_env"])
77+
78+
break # breaks the pytest_toml_name forloop
79+
if has_toml: # breaks the path forloop
6880
break
6981

70-
if not has_toml_conf:
71-
for line in early_config.getini("env"):
72-
# INI lines e.g. D:R:NAME=VAL has two flags (R and D), NAME key, and VAL value
73-
parts = line.partition("=")
74-
ini_key_parts = parts[0].split(":")
75-
flags = {k.strip().upper() for k in ini_key_parts[:-1]}
76-
# R: is a way to designate whether to use raw value -> perform no transformation of the value
77-
transform = "R" not in flags
78-
# D: is a way to mark the value to be set only if it does not exist yet
79-
skip_if_set = "D" in flags
80-
key = ini_key_parts[-1].strip()
81-
value = parts[2].strip()
82-
yield Entry(key, value, transform, skip_if_set)
82+
if has_toml:
83+
return
84+
85+
for line in early_config.getini("env"):
86+
# INI lines e.g. D:R:NAME=VAL has two flags (R and D), NAME key, and VAL value
87+
parts = line.partition("=")
88+
ini_key_parts = parts[0].split(":")
89+
flags = {k.strip().upper() for k in ini_key_parts[:-1]}
90+
# R: is a way to designate whether to use raw value -> perform no transformation of the value
91+
transform = "R" not in flags
92+
# D: is a way to mark the value to be set only if it does not exist yet
93+
skip_if_set = "D" in flags
94+
key = ini_key_parts[-1].strip()
95+
value = parts[2].strip()
96+
yield Entry(key, value, transform, skip_if_set)

tests/test_env.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,59 +119,128 @@ def test_env_via_pytest(
119119

120120

121121
@pytest.mark.parametrize(
122-
("env", "toml", "ini", "expected_env"),
122+
("env", "pyproject_toml", "pytest_toml", "ini", "expected_env", "pytest_toml_name"),
123123
[
124124
pytest.param(
125125
{},
126126
'[tool.pytest.ini_options]\nenv = ["MAGIC=toml", "MAGIC_2=toml2"]',
127+
"",
127128
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
128129
{"MAGIC": "ini", "MAGIC_2": "ini2"},
129-
id="ini over toml ini_options",
130+
None,
131+
id="ini over pyproject toml ini_options",
130132
),
131133
pytest.param(
132134
{},
133135
'[tool.pytest.ini_options]\nenv = ["MAGIC=toml", "MAGIC_2=toml2"]',
134136
"",
137+
"",
135138
{"MAGIC": "toml", "MAGIC_2": "toml2"},
136-
id="toml via ini_options",
139+
None,
140+
id="pyproject toml via ini_options",
137141
),
138142
pytest.param(
139143
{},
140144
'[tool.pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
141145
"",
146+
"",
147+
{"MAGIC": "1", "MAGIC_2": "toml2"},
148+
None,
149+
id="pyproject toml native",
150+
),
151+
pytest.param(
152+
{},
153+
"",
154+
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
155+
"",
156+
{"MAGIC": "1", "MAGIC_2": "toml2"},
157+
"pytest.toml",
158+
id="pytest toml",
159+
),
160+
pytest.param(
161+
{},
162+
"",
163+
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
164+
"",
142165
{"MAGIC": "1", "MAGIC_2": "toml2"},
143-
id="toml native",
166+
".pytest.toml",
167+
id="hidden pytest toml",
144168
),
145169
pytest.param(
146170
{},
147171
'[tool.pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
172+
"",
148173
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
149174
{"MAGIC": "1", "MAGIC_2": "toml2"},
150-
id="toml native over ini",
175+
None,
176+
id="pyproject toml native over ini",
177+
),
178+
pytest.param(
179+
{},
180+
"",
181+
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
182+
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
183+
{"MAGIC": "1", "MAGIC_2": "toml2"},
184+
"pytest.toml",
185+
id="pytest toml native over ini",
186+
),
187+
pytest.param(
188+
{},
189+
"",
190+
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
191+
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
192+
{"MAGIC": "1", "MAGIC_2": "toml2"},
193+
".pytest.toml",
194+
id="hidden pytest toml native over ini",
151195
),
152196
pytest.param(
153197
{},
154198
'[tool.pytest_env]\nMAGIC = {value = "toml", "transform"= true, "skip_if_set" = true}',
155199
"",
200+
"",
156201
{"MAGIC": "toml"},
157-
id="toml inline table",
202+
None,
203+
id="pyproject toml inline table",
204+
),
205+
pytest.param(
206+
{},
207+
"",
208+
'[pytest_env]\nMAGIC = {value = "toml", "transform"= true, "skip_if_set" = true}',
209+
"",
210+
{"MAGIC": "toml"},
211+
"pytest.toml",
212+
id="pytest toml inline table",
213+
),
214+
pytest.param(
215+
{},
216+
'[tool.pytest_env]\nMAGIC = 1\nMAGIC_2 = "pyproject"',
217+
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "pytest"',
218+
"",
219+
{"MAGIC": "1", "MAGIC_2": "pytest"},
220+
"pytest.toml",
221+
id="pytest toml over pyproject toml",
158222
),
159223
],
160224
)
161225
def test_env_via_toml( # noqa: PLR0913, PLR0917
162226
testdir: pytest.Testdir,
163227
env: dict[str, str],
164-
toml: str,
228+
pyproject_toml: str,
229+
pytest_toml: str,
165230
ini: str,
166231
expected_env: dict[str, str],
232+
pytest_toml_name: str | None,
167233
request: pytest.FixtureRequest,
168234
) -> None:
169235
tmp_dir = Path(str(testdir.tmpdir))
170236
test_name = re.sub(r"\W|^(?=\d)", "_", request.node.callspec.id).lower()
171237
Path(str(tmp_dir / f"test_{test_name}.py")).symlink_to(Path(__file__).parent / "template.py")
172238
if ini:
173239
(tmp_dir / "pytest.ini").write_text(ini, encoding="utf-8")
174-
(tmp_dir / "pyproject.toml").write_text(toml, encoding="utf-8")
240+
if pyproject_toml:
241+
(tmp_dir / "pyproject.toml").write_text(pyproject_toml, encoding="utf-8")
242+
if pytest_toml and pytest_toml_name:
243+
(tmp_dir / pytest_toml_name).write_text(pytest_toml, encoding="utf-8")
175244

176245
new_env = {
177246
**env,
@@ -187,8 +256,9 @@ def test_env_via_toml( # noqa: PLR0913, PLR0917
187256
result.assert_outcomes(passed=1)
188257

189258

190-
def test_env_via_toml_bad(testdir: pytest.Testdir) -> None:
191-
toml_file = Path(str(testdir.tmpdir)) / "pyproject.toml"
259+
@pytest.mark.parametrize("toml_name", ["pytest.toml", ".pytest.toml", "pyproject.toml"])
260+
def test_env_via_pyproject_toml_bad(testdir: pytest.Testdir, toml_name: str) -> None:
261+
toml_file = Path(str(testdir.tmpdir)) / toml_name
192262
toml_file.write_text("bad toml", encoding="utf-8")
193263

194264
result = testdir.runpytest()

0 commit comments

Comments
 (0)