Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ versions](https://img.shields.io/pypi/pyversions/pytest-env.svg)](https://pypi.o
[![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)
[![Downloads](https://static.pepy.tech/badge/pytest-env/month)](https://pepy.tech/project/pytest-env)

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

## Installation

Expand All @@ -18,7 +18,14 @@ pip install pytest-env

## Usage

### Native form in `pyproject.toml`
### Native form in `pyproject.toml`, `pytest.toml` and `.pytest.toml`

> [!NOTE]
> `pytest.toml` and `.pytest.toml` is only supported on Pytest 9.0+.

Native form takes precedence over the `pytest.ini` form. `pytest.toml` takes precedence over `.pytest.toml`, and that takes precedence over `pyproject.toml`.

In `pyproject.toml`:

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

The `tool.pytest_env` tables keys are the environment variables keys to set. The right hand side of the assignment:
In `pytest.toml` (or `.pytest.toml`):

```toml
[pytest_env]
HOME = "~/tmp"
RUN_ENV = 1
TRANSFORMED = {value = "{USER}/alpha", transform = true}
SKIP_IF_SET = {value = "on", skip_if_set = true}
```

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:

- if an inline table you can set options via the `transform` or `skip_if_set` keys, while the `value` key holds the
value to set (or transform before setting). For transformation the variables you can use is other environment
Expand Down
74 changes: 44 additions & 30 deletions src/pytest_env/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import sys
from dataclasses import dataclass
from itertools import chain
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

import pytest

if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Generator, Iterator

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


def _parse_toml_config(config: dict[str, Any]) -> Generator[Entry, None, None]:
for key, entry in config.items():
if isinstance(entry, dict):
value = str(entry["value"])
transform, skip_if_set = bool(entry.get("transform")), bool(entry.get("skip_if_set"))
else:
value, transform, skip_if_set = str(entry), False, False
yield Entry(key, value, transform, skip_if_set)


def _load_values(early_config: pytest.Config) -> Iterator[Entry]:
has_toml_conf = False
for path in chain.from_iterable([[early_config.rootpath], early_config.rootpath.parents]): # noqa: PLR1702
toml_file = path / "pyproject.toml"
if toml_file.exists():
with toml_file.open("rb") as file_handler:
config = tomllib.load(file_handler)
if "tool" in config and "pytest_env" in config["tool"]:
has_toml_conf = True
for key, entry in config["tool"]["pytest_env"].items():
if isinstance(entry, dict):
value = str(entry["value"])
transform, skip_if_set = bool(entry.get("transform")), bool(entry.get("skip_if_set"))
else:
value, transform, skip_if_set = str(entry), False, False
yield Entry(key, value, transform, skip_if_set)
has_toml = False
for path in chain.from_iterable([[early_config.rootpath], early_config.rootpath.parents]):
for pytest_toml_name in ("pytest.toml", ".pytest.toml", "pyproject.toml"):
pytest_toml_file = path / pytest_toml_name
if pytest_toml_file.exists():
with pytest_toml_file.open("rb") as file_handler:
config = tomllib.load(file_handler)

if pytest_toml_name == "pyproject.toml": # in pyproject.toml the path is tool.pytest_env
config = config.get("tool", {})

if "pytest_env" in config:
has_toml = True
yield from _parse_toml_config(config["pytest_env"])

break # breaks the pytest_toml_name forloop
if has_toml: # breaks the path forloop
break

if not has_toml_conf:
for line in early_config.getini("env"):
# INI lines e.g. D:R:NAME=VAL has two flags (R and D), NAME key, and VAL value
parts = line.partition("=")
ini_key_parts = parts[0].split(":")
flags = {k.strip().upper() for k in ini_key_parts[:-1]}
# R: is a way to designate whether to use raw value -> perform no transformation of the value
transform = "R" not in flags
# D: is a way to mark the value to be set only if it does not exist yet
skip_if_set = "D" in flags
key = ini_key_parts[-1].strip()
value = parts[2].strip()
yield Entry(key, value, transform, skip_if_set)
if has_toml:
return

for line in early_config.getini("env"):
# INI lines e.g. D:R:NAME=VAL has two flags (R and D), NAME key, and VAL value
parts = line.partition("=")
ini_key_parts = parts[0].split(":")
flags = {k.strip().upper() for k in ini_key_parts[:-1]}
# R: is a way to designate whether to use raw value -> perform no transformation of the value
transform = "R" not in flags
# D: is a way to mark the value to be set only if it does not exist yet
skip_if_set = "D" in flags
key = ini_key_parts[-1].strip()
value = parts[2].strip()
yield Entry(key, value, transform, skip_if_set)
90 changes: 80 additions & 10 deletions tests/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,59 +119,128 @@ def test_env_via_pytest(


@pytest.mark.parametrize(
("env", "toml", "ini", "expected_env"),
("env", "pyproject_toml", "pytest_toml", "ini", "expected_env", "pytest_toml_name"),
[
pytest.param(
{},
'[tool.pytest.ini_options]\nenv = ["MAGIC=toml", "MAGIC_2=toml2"]',
"",
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
{"MAGIC": "ini", "MAGIC_2": "ini2"},
id="ini over toml ini_options",
None,
id="ini over pyproject toml ini_options",
),
pytest.param(
{},
'[tool.pytest.ini_options]\nenv = ["MAGIC=toml", "MAGIC_2=toml2"]',
"",
"",
{"MAGIC": "toml", "MAGIC_2": "toml2"},
id="toml via ini_options",
None,
id="pyproject toml via ini_options",
),
pytest.param(
{},
'[tool.pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
"",
"",
{"MAGIC": "1", "MAGIC_2": "toml2"},
None,
id="pyproject toml native",
),
pytest.param(
{},
"",
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
"",
{"MAGIC": "1", "MAGIC_2": "toml2"},
"pytest.toml",
id="pytest toml",
),
pytest.param(
{},
"",
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
"",
{"MAGIC": "1", "MAGIC_2": "toml2"},
id="toml native",
".pytest.toml",
id="hidden pytest toml",
),
pytest.param(
{},
'[tool.pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
"",
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
{"MAGIC": "1", "MAGIC_2": "toml2"},
id="toml native over ini",
None,
id="pyproject toml native over ini",
),
pytest.param(
{},
"",
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
{"MAGIC": "1", "MAGIC_2": "toml2"},
"pytest.toml",
id="pytest toml native over ini",
),
pytest.param(
{},
"",
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "toml2"',
"[pytest]\nenv = MAGIC=ini\n MAGIC_2=ini2",
{"MAGIC": "1", "MAGIC_2": "toml2"},
".pytest.toml",
id="hidden pytest toml native over ini",
),
pytest.param(
{},
'[tool.pytest_env]\nMAGIC = {value = "toml", "transform"= true, "skip_if_set" = true}',
"",
"",
{"MAGIC": "toml"},
id="toml inline table",
None,
id="pyproject toml inline table",
),
pytest.param(
{},
"",
'[pytest_env]\nMAGIC = {value = "toml", "transform"= true, "skip_if_set" = true}',
"",
{"MAGIC": "toml"},
"pytest.toml",
id="pytest toml inline table",
),
pytest.param(
{},
'[tool.pytest_env]\nMAGIC = 1\nMAGIC_2 = "pyproject"',
'[pytest_env]\nMAGIC = 1\nMAGIC_2 = "pytest"',
"",
{"MAGIC": "1", "MAGIC_2": "pytest"},
"pytest.toml",
id="pytest toml over pyproject toml",
),
],
)
def test_env_via_toml( # noqa: PLR0913, PLR0917
testdir: pytest.Testdir,
env: dict[str, str],
toml: str,
pyproject_toml: str,
pytest_toml: str,
ini: str,
expected_env: dict[str, str],
pytest_toml_name: str | None,
request: pytest.FixtureRequest,
) -> None:
tmp_dir = Path(str(testdir.tmpdir))
test_name = re.sub(r"\W|^(?=\d)", "_", request.node.callspec.id).lower()
Path(str(tmp_dir / f"test_{test_name}.py")).symlink_to(Path(__file__).parent / "template.py")
if ini:
(tmp_dir / "pytest.ini").write_text(ini, encoding="utf-8")
(tmp_dir / "pyproject.toml").write_text(toml, encoding="utf-8")
if pyproject_toml:
(tmp_dir / "pyproject.toml").write_text(pyproject_toml, encoding="utf-8")
if pytest_toml and pytest_toml_name:
(tmp_dir / pytest_toml_name).write_text(pytest_toml, encoding="utf-8")

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


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

result = testdir.runpytest()
Expand Down