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

Feature: composite scripts #1117

Merged
merged 1 commit into from
Jun 10, 2022
Merged
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
38 changes: 37 additions & 1 deletion docs/docs/usage/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ $ pdm run start -h 0.0.0.0
Flask server started at http://0.0.0.0:54321
```

PDM supports 3 types of scripts:
PDM supports 4 types of scripts:

### `cmd`

Expand Down Expand Up @@ -85,6 +85,32 @@ The function can be supplied with literal arguments:
foobar = {call = "foo_package.bar_module:main('dev')"}
```

### `composite`

This script kind execute other defined scripts:

```toml
[tool.pdm.scripts]
lint = "flake8"
test = "pytest"
all = {composite = ["lint", "test"]}
```

Running `pdm run all` will run `lint` first and then `test` if `lint` succeeded.

You can also provide arguments to the called scripts:

```toml
[tool.pdm.scripts]
lint = "flake8"
test = "pytest"
all = {composite = ["lint mypackage/", "test -v tests/"]}
```

!!! note
Argument passed on the command line are given to each called task.


### `env`

All environment variables set in the current shell can be seen by `pdm run` and will be expanded when executed.
Expand All @@ -98,6 +124,9 @@ start.env = {FOO = "bar", FLASK_ENV = "development"}

Note how we use [TOML's syntax](https://github.com/toml-lang/toml) to define a composite dictionary.

!!! note
Environment variables specified on a composite task level will override those defined by called tasks.

### `env_file`

You can also store all environment variables in a dotenv file and let PDM read it:
Expand All @@ -108,6 +137,9 @@ start.cmd = "flask run -p 54321"
start.env_file = ".env"
```

!!! note
A dotenv file specified on a composite task level will override those defined by called tasks.

### `site_packages`

To make sure the running environment is properly isolated from the outer Python interpreter,
Expand Down Expand Up @@ -183,3 +215,7 @@ Under certain situations PDM will look for some special hook scripts for executi
If there exists an `install` scripts under `[tool.pdm.scripts]` table, `pre_install`
scripts can be triggered by both `pdm install` and `pdm run install`. So it is
recommended to not use the preserved names.

!!! note
Composite tasks can also have pre and post scripts.
Called tasks will run their own pre and post scripts.
1 change: 1 addition & 0 deletions news/1117.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a `composite` script kind allowing to run multiple defined scripts in a single command as well as reusing scripts but overriding `env` or `env_file`.
55 changes: 41 additions & 14 deletions pdm/cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ class TaskOptions(TypedDict, total=False):
site_packages: bool


def exec_opts(*options: TaskOptions | None) -> dict[str, Any]:
return dict(
env={k: v for opts in options if opts for k, v in opts.get("env", {}).items()},
**{
k: v
for opts in options
if opts
for k, v in opts.items()
if k not in ("env", "help")
},
)


class Task(NamedTuple):
kind: str
name: str
Expand All @@ -38,7 +51,7 @@ def __str__(self) -> str:
class TaskRunner:
"""The task runner for pdm project"""

TYPES = ["cmd", "shell", "call"]
TYPES = ["cmd", "shell", "call", "composite"]
OPTIONS = ["env", "env_file", "help", "site_packages"]

def __init__(self, project: Project) -> None:
Expand Down Expand Up @@ -156,9 +169,10 @@ def _run_process(
pass
return process.returncode

def _run_task(self, task: Task, args: Sequence[str] = ()) -> int:
def _run_task(
self, task: Task, args: Sequence[str] = (), opts: TaskOptions | None = None
) -> int:
kind, _, value, options = task
options.pop("help", None)
shell = False
if kind == "cmd":
if not isinstance(value, list):
Expand All @@ -184,38 +198,51 @@ def _run_task(self, task: Task, args: Sequence[str] = ()) -> int:
f"import sys, {module} as {short_name};"
f"sys.exit({short_name}.{func})",
] + list(args)
if "env" in self.global_options:
options["env"] = {**self.global_options["env"], **options.get("env", {})}
options["env_file"] = options.get(
"env_file", self.global_options.get("env_file")
)
elif kind == "composite":
assert isinstance(value, list)

self.project.core.ui.echo(
f"Running {task}: [green]{str(args)}[/]",
err=True,
verbosity=termui.Verbosity.DETAIL,
)
if kind == "composite":
for script in value:
splitted = shlex.split(script)
cmd = splitted[0]
subargs = splitted[1:] + args # type: ignore
code = self.run(cmd, subargs, options)
if code != 0:
return code
return code
return self._run_process(
args, chdir=True, shell=shell, **options # type: ignore
args,
chdir=True,
shell=shell,
**exec_opts(self.global_options, options, opts),
)

def run(self, command: str, args: Sequence[str]) -> int:
def run(
self, command: str, args: Sequence[str], opts: TaskOptions | None = None
) -> int:
task = self._get_task(command)
if task is not None:
pre_task = self._get_task(f"pre_{command}")
if pre_task is not None:
code = self._run_task(pre_task)
code = self._run_task(pre_task, opts=opts)
if code != 0:
return code
code = self._run_task(task, args)
code = self._run_task(task, args, opts=opts)
if code != 0:
return code
post_task = self._get_task(f"post_{command}")
if post_task is not None:
code = self._run_task(post_task)
code = self._run_task(post_task, opts=opts)
return code
else:
return self._run_process(
[command] + args, **self.global_options # type: ignore
[command] + args, # type: ignore
**exec_opts(self.global_options, opts),
)

def show_list(self) -> None:
Expand Down
Loading