Skip to content

Commit

Permalink
feat(scripts): added composite tasks support (#1117)
Browse files Browse the repository at this point in the history
  • Loading branch information
noirbizarre authored and frostming committed Jun 24, 2022
1 parent 1ef97c5 commit 0ea660f
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 15 deletions.
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 @@ -26,6 +26,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 @@ -39,7 +52,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 @@ -161,9 +174,10 @@ def _run_process(
signal.signal(signal.SIGINT, s)
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 @@ -189,38 +203,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

0 comments on commit 0ea660f

Please sign in to comment.