Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add new command option to set env file for pdm script #3358

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/usage/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions news/3355.feat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new command option to set env file for pdm script.
20 changes: 15 additions & 5 deletions src/pdm/cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -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",
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/pdm/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
28 changes: 28 additions & 0 deletions tests/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = {
Expand Down
Loading