Skip to content

Commit 83f64b1

Browse files
Elijasgaborbernat
andauthored
Add R: prefix implementation and unit tests (#9)
Co-authored-by: Bernát Gábor <[email protected]>
1 parent 59d909b commit 83f64b1

File tree

6 files changed

+152
-39
lines changed

6 files changed

+152
-39
lines changed

README.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ versions](https://img.shields.io/pypi/pyversions/pytest-env.svg)](https://pypi.o
88
black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
99
[![Downloads](https://pepy.tech/badge/pytest-env/month)](https://pepy.tech/project/pytest-env/month)
1010

11-
This is a py.test plugin that enables you to set environment variables in the pytest.ini file.
11+
This is a `pytest` plugin that enables you to set environment variables in the pytest.ini file.
1212

1313
## Installation
1414

@@ -20,7 +20,8 @@ pip install pytest-env
2020

2121
## Usage
2222

23-
In your pytest.ini file add a key value pair with `env` as the key and the environment variables as a line separated list of `KEY=VALUE` entries. The defined variables will be added to the environment before any tests are run:
23+
In your pytest.ini file add a key value pair with `env` as the key and the environment variables as a line separated
24+
list of `KEY=VALUE` entries. The defined variables will be added to the environment before any tests are run:
2425

2526
```ini
2627
[pytest]
@@ -29,6 +30,18 @@ env =
2930
RUN_ENV=test
3031
```
3132

33+
Or with `pyproject.toml`:
34+
35+
```toml
36+
[tool.pytest.ini_options]
37+
env = [
38+
"HOME=~/tmp",
39+
"RUN_ENV=test",
40+
]
41+
```
42+
43+
### Only set if not already set
44+
3245
You can use `D:` (default) as prefix if you don't want to override existing environment variables:
3346

3447
```ini
@@ -38,10 +51,23 @@ env =
3851
D:RUN_ENV=test
3952
```
4053

41-
Lastly, you can use existing environment variables using a python-like format:
54+
### Transformation
55+
56+
You can use existing environment variables using a python-like format, these environment variables will be expended
57+
before setting the environment variable:
4258

4359
```ini
4460
[pytest]
4561
env =
4662
RUN_PATH=/run/path/{USER}
4763
```
64+
65+
You can apply the `R:` prefix to keep the raw value and skip this transformation step (can combine with the `D:` flag,
66+
order is not important):
67+
68+
```ini
69+
[pytest]
70+
env =
71+
R:RUN_PATH=/run/path/{USER}
72+
R:D:RUN_PATH_IF_NOT_SET=/run/path/{USER}
73+
```

src/pytest_env/plugin.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
def pytest_addoption(parser: pytest.Parser) -> None:
1010
"""Add section to configuration files."""
11-
help_msg = "a line separated list of environment variables " "of the form NAME=VALUE."
12-
11+
help_msg = "a line separated list of environment variables of the form (FLAG:)NAME=VALUE"
1312
parser.addini("env", type="linelist", help=help_msg, default=[])
1413

1514

@@ -19,20 +18,19 @@ def pytest_load_initial_conftests(
1918
) -> None:
2019
"""Load environment variables from configuration files."""
2120
for line in early_config.getini("env"):
22-
part = line.partition("=")
23-
key = part[0].strip()
24-
value = part[2].strip()
25-
26-
# Replace environment variables in value. for instance TEST_DIR={USER}/repo_test_dir.
27-
value = value.format(**os.environ)
28-
29-
# use D: as a way to designate a default value that will only override env variables if they do not exist
30-
default_key = key.split("D:")
31-
default_val = False
32-
33-
if len(default_key) == 2:
34-
key = default_key[1]
35-
default_val = True
3621

37-
if not default_val or key not in os.environ:
38-
os.environ[key] = value
22+
# INI lines e.g. D:R:NAME=VAL has two flags (R and D), NAME key, and VAL value
23+
parts = line.partition("=")
24+
ini_key_parts = parts[0].split(":")
25+
flags = {k.strip().upper() for k in ini_key_parts[:-1]}
26+
# R: is a way to designate whether to use raw value -> perform no transformation of the value
27+
transform = "R" not in flags
28+
# D: is a way to mark the value to be set only if it does not exist yet
29+
skip_if_set = "D" in flags
30+
key = ini_key_parts[-1].strip()
31+
value = parts[2].strip()
32+
33+
if skip_if_set and key in os.environ:
34+
continue
35+
# transformation -> replace environment variables, e.g. TEST_DIR={USER}/repo_test_dir.
36+
os.environ[key] = value.format(**os.environ) if transform else value

tests/example.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

tests/template.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
import os
5+
6+
7+
def test_env() -> None:
8+
for key, value in ast.literal_eval(os.environ["_TEST_ENV"]).items():
9+
assert os.environ[key] == value, key

tests/test_env.py

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,106 @@
11
from __future__ import annotations
22

3+
import os
4+
import re
35
from pathlib import Path
6+
from unittest import mock
47

58
import pytest
69

710

8-
@pytest.fixture()
9-
def example(testdir: pytest.Testdir) -> pytest.Testdir:
10-
src = Path(__file__).parent / "example.py"
11-
dest = Path(str(testdir.tmpdir / "test_example.py"))
12-
dest.symlink_to(src)
13-
return testdir
11+
@pytest.mark.parametrize(
12+
("env", "ini", "expected_env"),
13+
[
14+
pytest.param(
15+
{},
16+
"[pytest]\nenv = MAGIC=alpha",
17+
{"MAGIC": "alpha"},
18+
id="new key - add to env",
19+
),
20+
pytest.param(
21+
{},
22+
"[pytest]\nenv = MAGIC=alpha\n SORCERY=beta",
23+
{"MAGIC": "alpha", "SORCERY": "beta"},
24+
id="two new keys - add to env",
25+
),
26+
pytest.param(
27+
# This test also tests for non-interference of env variables between this test and tests above
28+
{},
29+
"[pytest]\nenv = d:MAGIC=beta",
30+
{"MAGIC": "beta"},
31+
id="D flag - add to env",
32+
),
33+
pytest.param(
34+
{"MAGIC": "alpha"},
35+
"[pytest]\nenv = MAGIC=beta",
36+
{"MAGIC": "beta"},
37+
id="key exists in env - overwrite",
38+
),
39+
pytest.param(
40+
{"MAGIC": "alpha"},
41+
"[pytest]\nenv = D:MAGIC=beta",
42+
{"MAGIC": "alpha"},
43+
id="D exists - original val kept",
44+
),
45+
pytest.param(
46+
{"PLANET": "world"},
47+
"[pytest]\nenv = MAGIC=hello_{PLANET}",
48+
{"MAGIC": "hello_world"},
49+
id="curly exist - interpolate var",
50+
),
51+
pytest.param(
52+
{"PLANET": "world"},
53+
"[pytest]\nenv = R:MAGIC=hello_{PLANET}",
54+
{"MAGIC": "hello_{PLANET}"},
55+
id="R exists - not interpolate var",
56+
),
57+
pytest.param(
58+
{"MAGIC": "a"},
59+
"[pytest]\nenv = R:MAGIC={MAGIC}b\n D:MAGIC={MAGIC}c\n MAGIC={MAGIC}d",
60+
{"MAGIC": "{MAGIC}bd"},
61+
id="incremental interpolation",
62+
),
63+
pytest.param(
64+
{"PLANET": "world"},
65+
"[pytest]\nenv = D:R:RESULT=hello_{PLANET}",
66+
{"RESULT": "hello_{PLANET}"},
67+
id="two flags",
68+
),
69+
pytest.param(
70+
{"PLANET": "world"},
71+
"[pytest]\nenv = R:D:RESULT=hello_{PLANET}",
72+
{"RESULT": "hello_{PLANET}"},
73+
id="two flags - reversed",
74+
),
75+
pytest.param(
76+
{"PLANET": "world"},
77+
"[pytest]\nenv = d:r:RESULT=hello_{PLANET}",
78+
{"RESULT": "hello_{PLANET}"},
79+
id="lowercase flags",
80+
),
81+
pytest.param(
82+
{"PLANET": "world"},
83+
"[pytest]\nenv = D : R : RESULT = hello_{PLANET}",
84+
{"RESULT": "hello_{PLANET}"},
85+
id="whitespace is ignored",
86+
),
87+
pytest.param(
88+
{"MAGIC": "zero"},
89+
"",
90+
{"MAGIC": "zero"},
91+
id="empty ini works",
92+
),
93+
],
94+
)
95+
def test_env(
96+
testdir: pytest.Testdir, env: dict[str, str], ini: str, expected_env: dict[str, str], request: pytest.FixtureRequest
97+
) -> None:
98+
test_name = re.sub(r"\W|^(?=\d)", "_", request.node.callspec.id).lower()
99+
Path(str(testdir.tmpdir / f"test_{test_name}.py")).symlink_to(Path(__file__).parent / "template.py")
100+
(testdir.tmpdir / "pytest.ini").write_text(ini, encoding="utf-8")
14101

102+
# monkeypatch persists env variables across parametrized tests, therefore using mock.patch.dict
103+
with mock.patch.dict(os.environ, {**env, "_TEST_ENV": repr(expected_env)}, clear=True):
104+
result = testdir.runpytest()
15105

16-
def test_simple(example: pytest.Testdir) -> None:
17-
(example.tmpdir / "pytest.ini").write_text("[pytest]\nenv = MAGIC=alpha", encoding="utf-8")
18-
example.monkeypatch.setenv("_PATCH", "alpha")
19-
result = example.runpytest()
20106
result.assert_outcomes(passed=1)

whitelist.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
addini
22
addoption
3+
callspec
34
conftests
45
getini
56
hookimpl
7+
parametrized
68
repo
79
runpytest
8-
setenv
910
testdir
1011
tmpdir
1112
tryfirst

0 commit comments

Comments
 (0)