diff --git a/README.md b/README.md index 3b3ae40..f9d5513 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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] @@ -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 @@ -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} +``` diff --git a/src/pytest_env/plugin.py b/src/pytest_env/plugin.py index 5f76405..5065057 100644 --- a/src/pytest_env/plugin.py +++ b/src/pytest_env/plugin.py @@ -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=[]) @@ -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 diff --git a/tests/example.py b/tests/example.py deleted file mode 100644 index ff568ea..0000000 --- a/tests/example.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import annotations - -import os - - -def test_works() -> None: - assert os.environ["MAGIC"] == os.environ["_PATCH"] diff --git a/tests/template.py b/tests/template.py new file mode 100644 index 0000000..e6712e4 --- /dev/null +++ b/tests/template.py @@ -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 diff --git a/tests/test_env.py b/tests/test_env.py index 2394e22..e52acb7 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -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) diff --git a/whitelist.txt b/whitelist.txt index 8312b84..52904f5 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -1,11 +1,12 @@ addini addoption +callspec conftests getini hookimpl +parametrized repo runpytest -setenv testdir tmpdir tryfirst