diff --git a/docs/docs/usage/scripts.md b/docs/docs/usage/scripts.md index 0a93faa4ef..4dd61794ab 100644 --- a/docs/docs/usage/scripts.md +++ b/docs/docs/usage/scripts.md @@ -146,6 +146,15 @@ start.cmd = "flask run -p 54321" start.env_file = ".env" ``` +The variables within the dotenv file will *not* override any existing environment variables. +If you want the dotenv file to override existing environment variables use the following: + +```toml +[tool.pdm.scripts] +start.cmd = "flask run -p 54321" +start.env_file.override = ".env" +``` + !!! note A dotenv file specified on a composite task level will override those defined by called tasks. diff --git a/news/1299.feature b/news/1299.feature new file mode 100644 index 0000000000..ce9bd61132 --- /dev/null +++ b/news/1299.feature @@ -0,0 +1,4 @@ +Add a env_file.override option that allows the user to specify that +the env_file should override any existing environment variables. This +is not the default as the environment the code runs it should take +precedence. diff --git a/pdm/cli/commands/run.py b/pdm/cli/commands/run.py index c8586c7c57..4fcf677c55 100644 --- a/pdm/cli/commands/run.py +++ b/pdm/cli/commands/run.py @@ -21,9 +21,13 @@ from pdm.utils import is_path_relative_to +class EnvFileOptions(TypedDict, total=True): + override: str + + class TaskOptions(TypedDict, total=False): env: Mapping[str, str] - env_file: str | None + env_file: EnvFileOptions | str | None help: str site_packages: bool @@ -119,12 +123,19 @@ def _run_process( shell: bool = False, site_packages: bool = False, env: Mapping[str, str] | None = None, - env_file: str | None = None, + env_file: EnvFileOptions | str | None = None, ) -> int: """Run command in a subprocess and return the exit code.""" project = self.project - process_env = {} - if env_file: + process_env = os.environ.copy() + if env_file is not None: + if isinstance(env_file, str): + path = env_file + override = False + else: + path = env_file["override"] + override = True + import dotenv project.core.ui.echo( @@ -132,10 +143,11 @@ def _run_process( err=True, verbosity=termui.Verbosity.DETAIL, ) - process_env = dotenv.dotenv_values( - project.root / env_file, encoding="utf-8" - ) - process_env.update(os.environ) + dotenv_env = dotenv.dotenv_values(project.root / path, encoding="utf-8") + if override: + process_env = {**process_env, **dotenv_env} + else: + process_env = {**dotenv_env, **process_env} pythonpath = process_env.get("PYTHONPATH", "").split(os.pathsep) pythonpath = [PEP582_PATH] + [ p for p in pythonpath if "pdm/pep582" not in p.replace("\\", "/") diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 0943b8a8a2..f95a75e830 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -230,20 +230,26 @@ def test_run_script_with_env_defined(project, invoke, capfd): assert capfd.readouterr()[0].strip() == "bar" -def test_run_script_with_dotenv_file(project, invoke, capfd): +def test_run_script_with_dotenv_file(project, invoke, capfd, monkeypatch): (project.root / "test_script.py").write_text( "import os; print(os.getenv('FOO'), os.getenv('BAR'))" ) project.tool_settings["scripts"] = { - "_": {"env": {"BAR": "foo"}}, - "test_script": {"cmd": "python test_script.py", "env_file": ".env"}, + "test_override": { + "cmd": "python test_script.py", + "env_file": {"override": ".env"}, + }, + "test_default": {"cmd": "python test_script.py", "env_file": ".env"}, } project.write_pyproject() + monkeypatch.setenv("BAR", "foo") (project.root / ".env").write_text("FOO=bar\nBAR=override") capfd.readouterr() with cd(project.root): - invoke(["run", "test_script"], obj=project) + invoke(["run", "test_default"], obj=project) assert capfd.readouterr()[0].strip() == "bar foo" + invoke(["run", "test_override"], obj=project) + assert capfd.readouterr()[0].strip() == "bar override" def test_run_script_override_global_env(project, invoke, capfd):