Skip to content

PDM Scripts#

Like npm run, with PDM, you can run arbitrary scripts or commands with local packages loaded.

Arbitrary Scripts#

1
pdm run flask run -p 54321

It will run flask run -p 54321 in the environment that is aware of packages in __pypackages__/ folder.

Note

There is a builtin shortcut making all scripts available as root commands as long as the script does not conflict with any builtin or plugin-contributed command. Said otherwise, if you have a test script, you can run both pdm run test and pdm test. But if you have an install script, only pdm run install will run it, pdm install will still run the builtin install command.

User Scripts#

PDM also supports custom script shortcuts in the optional [tool.pdm.scripts] section of pyproject.toml.

You can then run pdm run <script_name> to invoke the script in the context of your PDM project. For example:

1
2
[tool.pdm.scripts]
start = "flask run -p 54321"

And then in your terminal:

1
2
$ pdm run start
Flask server started at http://127.0.0.1:54321

Any following arguments will be appended to the command:

1
2
$ pdm run start -h 0.0.0.0
Flask server started at http://0.0.0.0:54321

PDM supports 4 types of scripts:

cmd#

Plain text scripts are regarded as normal command, or you can explicitly specify it:

1
2
[tool.pdm.scripts]
start = {cmd = "flask run -p 54321"}

In some cases, such as when wanting to add comments between parameters, it might be more convenient to specify the command as an array instead of a string:

1
2
3
4
5
6
7
[tool.pdm.scripts]
start = {cmd = [
    "flask",
    "run",
    # Important comment here about always using port 54321
    "-p", "54321"
]}

shell#

Shell scripts can be used to run more shell-specific tasks, such as pipeline and output redirecting. This is basically run via subprocess.Popen() with shell=True:

1
2
[tool.pdm.scripts]
filter_error = {shell = "cat error.log|grep CRITICAL > critical.log"}

call#

The script can be also defined as calling a python function in the form <module_name>:<func_name>:

1
2
[tool.pdm.scripts]
foobar = {call = "foo_package.bar_module:main"}

The function can be supplied with literal arguments:

1
2
[tool.pdm.scripts]
foobar = {call = "foo_package.bar_module:main('dev')"}

composite#

This script kind execute other defined scripts:

1
2
3
4
[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:

1
2
3
4
[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.

Script Options#

env#

All environment variables set in the current shell can be seen by pdm run and will be expanded when executed. Besides, you can also define some fixed environment variables in your pyproject.toml:

1
2
3
[tool.pdm.scripts]
start.cmd = "flask run -p 54321"
start.env = {FOO = "bar", FLASK_ENV = "development"}

Note how we use TOML's syntax 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:

1
2
3
[tool.pdm.scripts]
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, site-packages from the selected interpreter WON'T be loaded into sys.path, unless any of the following conditions holds:

  1. The executable is from PATH but not inside the __pypackages__ folder.
  2. -s/--site-packages flag is following pdm run.
  3. site_packages = true is in either the script table or the global setting key _.

Note that site-packages will always be loaded if running with PEP 582 enabled(without the pdm run prefix).

Shared Options#

If you want the options to be shared by all tasks run by pdm run, you can write them under a special key _ in [tool.pdm.scripts] table:

1
2
3
4
[tool.pdm.scripts]
_.env_file = ".env"
start = "flask run -p 54321"
migrate_db = "flask db upgrade"

Besides, inside the tasks, PDM_PROJECT_ROOT environment variable will be set to the project root.

Show the List of Scripts#

Use pdm run --list/-l to show the list of available script shortcuts:

1
2
3
4
5
6
7
8
$ pdm run --list
╭─────────────┬───────┬───────────────────────────╮
│ Name        │ Type  │ Description               │
├─────────────┼───────┼───────────────────────────┤
│ test_cmd    │ cmd   │ flask db upgrade          │
│ test_script │ call  │ call a python function    │
│ test_shell  │ shell │ shell command             │
╰─────────────┴───────┴───────────────────────────╯

You can add an help option with the description of the script, and it will be displayed in the Description column in the above output.

Pre & Post Scripts#

Like npm, PDM also supports tasks composition by pre and post scripts, pre script will be run before the given task and post script will be run after.

1
2
3
4
[tool.pdm.scripts]
pre_compress = "{{ Run BEFORE the `compress` script }}"
compress = "tar czvf compressed.tar.gz data/"
post_compress = "{{ Run AFTER the `compress` script }}"

In this example, pdm run compress will run all these 3 scripts sequentially.

The pipeline fails fast

In a pipeline of pre - self - post scripts, a failure will cancel the subsequent execution.

Hook Scripts#

Under certain situations PDM will look for some special hook scripts for execution:

  • post_init: Run after pdm init
  • pre_install: Run before installing packages
  • post_install: Run after packages are installed
  • pre_lock: Run before dependency resolution
  • post_lock: Run after dependency resolution
  • pre_build: Run before building distributions
  • post_build: Run after distributions are built
  • pre_publish: Run before publishinbg distributions
  • post_publish: Run after distributions are published
  • pre_script: Run before any script
  • post_script: Run after any script
  • pre_run: Run once before run script invocation
  • post_script: Run once after run script invocation

Note

Pre & post scripts can't receive any arguments.

Avoid name conflicts

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.

Skipping scripts#

Because, sometimes it is desirable to run a script but without its hooks or pre and post scripts, there is a --skip=:all which will disable all hooks, pre and post. There is also --skip=:pre and --skip=:post allowing to respectively skip all pre_* hooks and all post_* hooks.

It is also possible to need a pre script but not the post one, or to need all tasks from a composite tasks except one. For those use cases, there is a finer grained --skip parameter accepting a list of tasks or hooks name to exclude.

1
pdm run --skip pre_task1,task2 my-composite

This command will run the my-composite task and skip the pre_task1 hook as well as the task2 and its hooks.

You can also provide you skip list in PDM_SKIP_HOOKS environment variable but it will be overridden as soon as the --skip parameter is provided.

There is more details on hooks and pre/post scripts behavior on the dedicated hooks page.