Skip to content

Commit

Permalink
Add R: prefix implementation and unit tests (#9)
Browse files Browse the repository at this point in the history
Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com>
  • Loading branch information
Elijas and gaborbernat authored Oct 23, 2022
1 parent 59d909b commit 83f64b1
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 39 deletions.
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ versions](https://img.shields.io/pypi/pyversions/pytest-env.svg)](https://pypi.o
black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Downloads](https://pepy.tech/badge/pytest-env/month)](https://pepy.tech/project/pytest-env/month)

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

## Installation

Expand All @@ -20,7 +20,8 @@ pip install pytest-env

## Usage

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:
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:

```ini
[pytest]
Expand All @@ -29,6 +30,18 @@ env =
RUN_ENV=test
```

Or with `pyproject.toml`:

```toml
[tool.pytest.ini_options]
env = [
"HOME=~/tmp",
"RUN_ENV=test",
]
```

### Only set if not already set

You can use `D:` (default) as prefix if you don't want to override existing environment variables:

```ini
Expand All @@ -38,10 +51,23 @@ env =
D:RUN_ENV=test
```

Lastly, you can use existing environment variables using a python-like format:
### Transformation

You can use existing environment variables using a python-like format, these environment variables will be expended
before setting the environment variable:

```ini
[pytest]
env =
RUN_PATH=/run/path/{USER}
```

You can apply the `R:` prefix to keep the raw value and skip this transformation step (can combine with the `D:` flag,
order is not important):

```ini
[pytest]
env =
R:RUN_PATH=/run/path/{USER}
R:D:RUN_PATH_IF_NOT_SET=/run/path/{USER}
```
34 changes: 16 additions & 18 deletions src/pytest_env/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

def pytest_addoption(parser: pytest.Parser) -> None:
"""Add section to configuration files."""
help_msg = "a line separated list of environment variables " "of the form NAME=VALUE."

help_msg = "a line separated list of environment variables of the form (FLAG:)NAME=VALUE"
parser.addini("env", type="linelist", help=help_msg, default=[])


Expand All @@ -19,20 +18,19 @@ def pytest_load_initial_conftests(
) -> None:
"""Load environment variables from configuration files."""
for line in early_config.getini("env"):
part = line.partition("=")
key = part[0].strip()
value = part[2].strip()

# Replace environment variables in value. for instance TEST_DIR={USER}/repo_test_dir.
value = value.format(**os.environ)

# use D: as a way to designate a default value that will only override env variables if they do not exist
default_key = key.split("D:")
default_val = False

if len(default_key) == 2:
key = default_key[1]
default_val = True

if not default_val or key not in os.environ:
os.environ[key] = value
# 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()

if skip_if_set and key in os.environ:
continue
# transformation -> replace environment variables, e.g. TEST_DIR={USER}/repo_test_dir.
os.environ[key] = value.format(**os.environ) if transform else value
7 changes: 0 additions & 7 deletions tests/example.py

This file was deleted.

9 changes: 9 additions & 0 deletions tests/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import annotations

import ast
import os


def test_env() -> None:
for key, value in ast.literal_eval(os.environ["_TEST_ENV"]).items():
assert os.environ[key] == value, key
106 changes: 96 additions & 10 deletions tests/test_env.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,106 @@
from __future__ import annotations

import os
import re
from pathlib import Path
from unittest import mock

import pytest


@pytest.fixture()
def example(testdir: pytest.Testdir) -> pytest.Testdir:
src = Path(__file__).parent / "example.py"
dest = Path(str(testdir.tmpdir / "test_example.py"))
dest.symlink_to(src)
return testdir
@pytest.mark.parametrize(
("env", "ini", "expected_env"),
[
pytest.param(
{},
"[pytest]\nenv = MAGIC=alpha",
{"MAGIC": "alpha"},
id="new key - add to env",
),
pytest.param(
{},
"[pytest]\nenv = MAGIC=alpha\n SORCERY=beta",
{"MAGIC": "alpha", "SORCERY": "beta"},
id="two new keys - add to env",
),
pytest.param(
# This test also tests for non-interference of env variables between this test and tests above
{},
"[pytest]\nenv = d:MAGIC=beta",
{"MAGIC": "beta"},
id="D flag - add to env",
),
pytest.param(
{"MAGIC": "alpha"},
"[pytest]\nenv = MAGIC=beta",
{"MAGIC": "beta"},
id="key exists in env - overwrite",
),
pytest.param(
{"MAGIC": "alpha"},
"[pytest]\nenv = D:MAGIC=beta",
{"MAGIC": "alpha"},
id="D exists - original val kept",
),
pytest.param(
{"PLANET": "world"},
"[pytest]\nenv = MAGIC=hello_{PLANET}",
{"MAGIC": "hello_world"},
id="curly exist - interpolate var",
),
pytest.param(
{"PLANET": "world"},
"[pytest]\nenv = R:MAGIC=hello_{PLANET}",
{"MAGIC": "hello_{PLANET}"},
id="R exists - not interpolate var",
),
pytest.param(
{"MAGIC": "a"},
"[pytest]\nenv = R:MAGIC={MAGIC}b\n D:MAGIC={MAGIC}c\n MAGIC={MAGIC}d",
{"MAGIC": "{MAGIC}bd"},
id="incremental interpolation",
),
pytest.param(
{"PLANET": "world"},
"[pytest]\nenv = D:R:RESULT=hello_{PLANET}",
{"RESULT": "hello_{PLANET}"},
id="two flags",
),
pytest.param(
{"PLANET": "world"},
"[pytest]\nenv = R:D:RESULT=hello_{PLANET}",
{"RESULT": "hello_{PLANET}"},
id="two flags - reversed",
),
pytest.param(
{"PLANET": "world"},
"[pytest]\nenv = d:r:RESULT=hello_{PLANET}",
{"RESULT": "hello_{PLANET}"},
id="lowercase flags",
),
pytest.param(
{"PLANET": "world"},
"[pytest]\nenv = D : R : RESULT = hello_{PLANET}",
{"RESULT": "hello_{PLANET}"},
id="whitespace is ignored",
),
pytest.param(
{"MAGIC": "zero"},
"",
{"MAGIC": "zero"},
id="empty ini works",
),
],
)
def test_env(
testdir: pytest.Testdir, env: dict[str, str], ini: str, expected_env: dict[str, str], request: pytest.FixtureRequest
) -> None:
test_name = re.sub(r"\W|^(?=\d)", "_", request.node.callspec.id).lower()
Path(str(testdir.tmpdir / f"test_{test_name}.py")).symlink_to(Path(__file__).parent / "template.py")
(testdir.tmpdir / "pytest.ini").write_text(ini, encoding="utf-8")

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

def test_simple(example: pytest.Testdir) -> None:
(example.tmpdir / "pytest.ini").write_text("[pytest]\nenv = MAGIC=alpha", encoding="utf-8")
example.monkeypatch.setenv("_PATCH", "alpha")
result = example.runpytest()
result.assert_outcomes(passed=1)
3 changes: 2 additions & 1 deletion whitelist.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
addini
addoption
callspec
conftests
getini
hookimpl
parametrized
repo
runpytest
setenv
testdir
tmpdir
tryfirst
Expand Down

0 comments on commit 83f64b1

Please sign in to comment.