diff --git a/docs/usage/scripts.md b/docs/usage/scripts.md index c217768764..3c89921a2e 100644 --- a/docs/usage/scripts.md +++ b/docs/usage/scripts.md @@ -232,7 +232,9 @@ Note how we use [TOML's syntax](https://github.com/toml-lang/toml) to define a c !!! note Environment variables specified on a composite task level will override those defined by called tasks. -### `env_file` +### Setup env vars from a file + +Note: **The env_file option will be deprecated in the future.** You can also store all environment variables in a dotenv file and let PDM read it: @@ -251,12 +253,19 @@ start.cmd = "flask run -p 54321" start.env_file.override = ".env" ``` +After v2.23.0, we introduced a new way to specify the env file: + +1. We will default load the env file in the project under the name convention `.env.${environment}`. The `${environment}` is the value of the `PDM_ENVIRONMENT` environment variable(or You can set it by using `-e/--environment` command option). If the variable is not set, it will default to `local`. +2. You can also specify the env file by using the `--env-file` command option. +3. We will not override the other env vars loaded from the sources loaded before. + !!! note "Environment variable loading order" Env vars loaded from different sources are loaded in the following order: 1. OS environment variables 2. Project environments such as `PDM_PROJECT_ROOT`, `PATH`, `VIRTUAL_ENV`, etc 3. Dotenv file specified by `env_file` + 4. Env file specified by `--env-file` CLI option or `.env.${environment}` file 4. Env vars mapping specified by `env` Env vars from the latter sources will override those from the former sources. diff --git a/news/3355.feat.md b/news/3355.feat.md new file mode 100644 index 0000000000..91c7dea9c8 --- /dev/null +++ b/news/3355.feat.md @@ -0,0 +1 @@ +Add new command option to set env file for pdm script. diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py index 2765f0c975..e9cf875c6a 100644 --- a/src/pdm/cli/commands/run.py +++ b/src/pdm/cli/commands/run.py @@ -16,7 +16,7 @@ from pdm import termui from pdm.cli.commands.base import BaseCommand from pdm.cli.hooks import HookManager -from pdm.cli.options import skip_option, venv_option +from pdm.cli.options import env_file_option, environment_option, skip_option, venv_option from pdm.cli.utils import check_project_file from pdm.exceptions import PdmUsageError from pdm.signals import pdm_signals @@ -139,7 +139,7 @@ class TaskRunner: TYPES = ("cmd", "shell", "call", "composite") OPTIONS = ("env", "env_file", "help", "keep_going", "working_dir", "site_packages") - def __init__(self, project: Project, hooks: HookManager) -> None: + def __init__(self, project: Project, hooks: HookManager, options: argparse.Namespace | None = None) -> None: self.project = project global_options = cast( "TaskOptions", @@ -148,6 +148,8 @@ def __init__(self, project: Project, hooks: HookManager) -> None: self.global_options = global_options.copy() self.recreate_env = False self.hooks = hooks + self.environment = options.environment if options else "" + self.env_file = options.env_file if options else "" def _get_script_env(self, script_file: str) -> BaseEnvironment: import hashlib @@ -240,6 +242,7 @@ def _run_process( this_path = project_env.get_paths()["scripts"] os.environ.update(project_env.process_env) if env_file is not None: + deprecation_warning("env_file option is deprecated, More Detail see.") if isinstance(env_file, str): path = env_file override = False @@ -253,6 +256,12 @@ def _run_process( verbosity=termui.Verbosity.DETAIL, ) dotenv.load_dotenv(self.project.root / path, override=override) + env_file = ".env" + if self.environment != "": + env_file = f"{env_file}.{self.environment}" + if self.env_file != "": + env_file = self.env_file + dotenv.load_dotenv(self.project.root / env_file, override=False) if env: os.environ.update(resolve_variables(env.items(), override=True)) if shell: @@ -463,7 +472,7 @@ def _fix_env_file(data: dict[str, Any]) -> dict[str, Any]: class Command(BaseCommand): """Run commands or scripts with local packages loaded""" - arguments = (*BaseCommand.arguments, skip_option, venv_option) + arguments = (*BaseCommand.arguments, skip_option, venv_option, env_file_option, environment_option) def add_arguments(self, parser: argparse.ArgumentParser) -> None: action = parser.add_mutually_exclusive_group() @@ -479,6 +488,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: action="store_true", help="Output all scripts infos in JSON", ) + exec = parser.add_argument_group("Execution parameters") exec.add_argument( "-s", @@ -499,9 +509,9 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: def get_runner(self, project: Project, hooks: HookManager, options: argparse.Namespace) -> TaskRunner: if (runner_cls := getattr(self, "runner_cls", None)) is not None: # pragma: no cover deprecation_warning("runner_cls attribute is deprecated, use get_runner method instead.") - runner = cast("type[TaskRunner]", runner_cls)(project, hooks) + runner = cast("type[TaskRunner]", runner_cls)(project, hooks, options) else: - runner = TaskRunner(project, hooks) + runner = TaskRunner(project, hooks, options) runner.recreate_env = options.recreate if options.site_packages: runner.global_options["site_packages"] = True diff --git a/src/pdm/cli/options.py b/src/pdm/cli/options.py index bea9548026..500e920dbc 100644 --- a/src/pdm/cli/options.py +++ b/src/pdm/cli/options.py @@ -475,6 +475,23 @@ def non_interactive_option( default=os.getenv("PDM_IN_VENV"), ) +environment_option = Option( + "-e", + "--environment", + dest="environment", + action="store", + default=os.getenv("PDM_ENVIRONMENT", "local"), + help="Specify the environment name to use. [env var: PDM_ENVIRONMENT]", +) + +env_file_option = Option( + "--env-file", + dest="env_file", + action="store", + help="Specify the environment file to use. [env var: PDM_ENV_FILE]", + default=os.getenv("PDM_ENV_FILE", ""), +) + lock_strategy_group = ArgumentGroup("Lock Strategy") lock_strategy_group.add_argument( diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 8080072856..4b781f3e68 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -410,6 +410,34 @@ def test_run_script_with_dotenv_file(project, pdm, capfd, monkeypatch): assert capfd.readouterr()[0].strip() == "bar override" +def test_run_script_with_dotenv_file_idetify_by_environment_variable(project, pdm, capfd, monkeypatch): + (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'), os.getenv('BAR'))") + project.pyproject.settings["scripts"] = { + "test_default": {"cmd": "python test_script.py"}, + } + (project.root / ".env.local").write_text("FOO=bar\nBAR=local") + (project.root / ".env.alpha").write_text("FOO=bar\nBAR=alpha") + with cd(project.root): + pdm(["run", "test_default"], obj=project) + assert capfd.readouterr()[0].strip() == "bar local" + pdm(["run", "-e", "alpha", "test_default"], obj=project) + assert capfd.readouterr()[0].strip() == "bar alpha" + + +def test_run_script_with_dotenv_file_with_env_file_option(project, pdm, capfd, monkeypatch): + (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'), os.getenv('BAR'))") + project.pyproject.settings["scripts"] = { + "test_default": {"cmd": "python test_script.py"}, + } + (project.root / ".env.local").write_text("FOO=bar\nBAR=local") + (project.root / ".env.alpha").write_text("FOO=bar\nBAR=alpha") + with cd(project.root): + pdm(["run", "test_default"], obj=project) + assert capfd.readouterr()[0].strip() == "bar local" + pdm(["run", "-e", "local", "--env-file", ".env.alpha", "test_default"], obj=project) + assert capfd.readouterr()[0].strip() == "bar alpha" + + def test_run_script_override_global_env(project, pdm, capfd): (project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))") project.pyproject.settings["scripts"] = {