uv icon indicating copy to clipboard operation
uv copied to clipboard

Using `uv run` as a task runner

Open my1e5 opened this issue 1 year ago • 225 comments

For those of us migrating over from Rye, one of its nice features is the built-in task runner using rye run and [tool.rye.scripts]. For example:

[tool.rye.scripts]
hello = "echo Hello from Rye!"
$ rye run hello
Hello from Rye!

It could have some more features - here is a selection of feature requests from the community:

  • https://github.com/astral-sh/rye/issues/695
  • https://github.com/astral-sh/rye/issues/652
  • https://github.com/astral-sh/rye/issues/930
  • https://github.com/astral-sh/rye/issues/1243

A lot of these requested features are things that other 3rd party tools currently offer. I thought it might be useful to highlight a few other tools here, in particular because they also integrate with the pyproject.toml ecosystem and can be used with uv today.

  • Poe the Poet

    https://github.com/nat-n/poethepoet

    uv add --dev poethepoet
    
    [tool.poe.tasks]
    hello = "echo Hello from poe!"
    
    $ uv run poe hello
    Poe => echo Hello from 'poe!'
    Hello from poe!
    
  • taskipy

    https://github.com/taskipy/taskipy

    uv add --dev taskipy
    
    [tool.taskipy.tasks]
    hello = "echo Hello from taskipy!"
    
    $ uv run task hello
    Hello from taskipy!
    

Perhaps these can serve as some inspiration for a future uv run task runner and also in the meantime offer a solution for people coming over from Rye looking for a way to run tasks.

my1e5 avatar Aug 08 '24 11:08 my1e5

Relevant comment from another issue: https://github.com/astral-sh/uv/issues/5632#issuecomment-2267115729

chrisrodrigue avatar Aug 08 '24 21:08 chrisrodrigue

PDM supports this: https://pdm-project.org/latest/usage/scripts/

chrisrodrigue avatar Aug 08 '24 21:08 chrisrodrigue

Yeah we plan to support something like this! We haven't spent time on the design yet.

charliermarsh avatar Aug 09 '24 02:08 charliermarsh

The pyproject standard already supports [project.scripts], so uv may not need to use its own table. https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#creating-executable-scripts

nikhilweee avatar Aug 09 '24 20:08 nikhilweee

[project.scripts] is a little different -- that's used to expose specific Python functions as executable scripts, and we do support that already.

charliermarsh avatar Aug 09 '24 21:08 charliermarsh

Perhaps naming this section tool.uv.tasks or tool.uv.aliases could help disambiguate that.

chrisrodrigue avatar Aug 09 '24 23:08 chrisrodrigue

Or maybe [tool.uv.run] to be consistent with the command uv run. Or we could even think about [tool.uv.commands]. I'm not a big fan of [tool.uv.scripts] since it conflicts with [project.scripts] and I myself got confused before.

nikhilweee avatar Aug 10 '24 21:08 nikhilweee

This is the main thing I missed coming from hatch: https://hatch.pypa.io/dev/config/environment/overview/#scripts

cdwilson avatar Aug 23 '24 17:08 cdwilson

+1 to @nikhilweee suggestions. I think “command” reflects the intent/concept.

Hatch has an “environment” concept and supports running commands namespaced to an environment like so

hatch run test:cov

where “test” is a user-defined environment (with a dependency group) and “cov” is a user-defined command for that environment.

[tool.hatch.envs.test]
dependencies = [
 "pytest",
 "pytest-cov",
 "pytest-mock",
 "freezegun",
]

[tool.hatch.envs.test.scripts]
cov = 'pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src'

[[tool.hatch.envs.test.matrix]]
python = ["3.8", "3.9", "3.10", "3.11", "3.12"]

I would be curious to hear the use cases of nesting dependency groups and commands into “environments” like this rather than defining them at the top-level (i.e. [tool.uv.commands]/[tool.uv.dev-dependencies]).

chrisrodrigue avatar Aug 23 '24 19:08 chrisrodrigue

since it has not been mentioned yet, adding as a possible inspiration for design of tasks also pixi: https://pixi.sh/latest/features/advanced_tasks/

pietroppeter avatar Aug 28 '24 10:08 pietroppeter

I happen to be writing a cross-project task runner that supports a bunch of formats (e.g. rye, pdm, package.json, Cargo.toml; even uv's workspace config).

For what it's worth, almost all python runners use tool.<name>.scripts for task config (presumably inspired by npm's package.json format), so it's somewhat of an easier upgrade path for people coming from other tools.

https://github.com/metaist/ds

metaist avatar Aug 28 '24 23:08 metaist

Also related to this thread, I wish uvx was uv run instead of uv tool run when inside a project.

uv tool run seems like something you would do to play with a tool, like ruff. But then you will eventually uv add ruff --dev and forever keep writing uv run ruff check instead of uvx ruff check which won't respect the locked version and will be in different virtualenv. It also means the tool could be running on a different Python (does not apply to ruff, but any other python lib) and all sorts of weird stuff can happen.

I know uv run stuff is still short, but it could be 4 keystrokes shorter.

Regarding uv run being a task runner it means people will type it waaaaay more often than uv tool run. I would appreciate a dedicated command like uvr

inoa-jboliveira avatar Sep 08 '24 04:09 inoa-jboliveira

@inoa-jboliveira I opened a dedicated issue for that https://github.com/astral-sh/uv/issues/7186

zanieb avatar Sep 08 '24 13:09 zanieb

Putting together some thoughts about semantics. This issue is about adding support for running arbitrary instructions specified in pyproject.toml. I deliberately use the term instruction to avoid using any of the other terms under consideration (command, tool, script, etc).

What do we call these instructions?

Lots of existing tools refer to them as "scripts".

  1. npm and yarn have first class support for scripts. Users can define them in package.json
  2. composer also uses the same term. Users can define them in composer.json
  3. pdm takes inspiration from npm and also uses the term scripts. Users can define them in [tool.pdm.scripts]
  4. rye follows suit. Custom scripts are defined in [tool.rye.scripts]
  5. hatch also uses the term scripts, although they are tied to environments. Defined in [tool.hatch.envs.<env>.scripts]

It seems advantageous to just go with the term "scripts" because it is the de-facto standard. As noted by another user https://github.com/astral-sh/uv/issues/5903#issuecomment-2316413223, this would also reduce friction for users coming to uv from other package managers. That said, this approach has a major flaw because it overlaps with the concept of entry points defined in [project.scripts]. Entry points expose certain python functions as executable scripts, but do not allow arbitrary commands. Furthermore, [project.scripts] has already been established in PEP-0621, as the official spec. So what about other terms?

Another option is to use the term "tasks"

  1. pixi uses the term "tasks". Users can define them in the [tasks] table in pixi.toml
  2. bundler uses rake tasks. Although it resembles entry points, sh is supported.
  3. grunt and gulp also use the term "tasks". Although they are task runners, not package managers.
  4. gradle uses the term "tasks", defined in build.gradle

Another option is to call them "executables". dart uses this term in pubspec.yaml

We could also use "commands", although I wasn't able to find existing tools which use this term.

After settling on a name, an obvious thing to do is to let users define instructions in the [tool.uv.<name>] table.

How do we invoke these instructions?

There are two options here.

  1. Overload existing uv run <instruction> (follows from npm run <script>)
  2. Add a new subcommand uv invoke / uv command / uv task

How should these instructions be specified?

PDM's documentation around user scripts is pretty evolved, with support for a bunch of features.

  1. cmd mode, shell mode, composite mode
  2. Specify env vars and env files for each script
  3. Specify working dir and site packages for each script
  4. Specify order of arguments
  5. Specify pre and post scripts
  6. call a function from a python script (entry point)

Rye has its own format, which is a subset of PDM features.

  1. Specify env vars and env files for each script
  2. chain multiple scripts one after the other
  3. call a function from a python script (entry point)

I hope this serves as a starter for discussing additional details for this feature.

nikhilweee avatar Sep 10 '24 20:09 nikhilweee

(Nice comment, thank you!)

charliermarsh avatar Sep 10 '24 21:09 charliermarsh

One nice thing about PDM is that if a command is not recognized as a built-in, it is treated as pdm run. Thus, pdm foobar would be shorthand for pdm run foobar, which executes the command defined in [tool.pdm.scripts.foobar].

Xdynix avatar Sep 10 '24 21:09 Xdynix

IMHO, pdm has the most extensive support for these scripts and I would personally like to see the same support in uv too. And if so, best to support the same pyproject section too :scream:. It's especially nice when one doesn't have to use any other tools like Makefiles (ugh).

Alas, one can argue that the pdm support for scripts is feature-creep for uv, in which case the rye model works too :)

gdamjan avatar Sep 10 '24 21:09 gdamjan

Furthermore, [project.scripts] has already been established in PEP-0621, as the official spec. So what about other terms?

I wonder if would be a good time to maybe standardise this? I don't know if a pep is required, but since we have some many package managers for python it would be nice if we can have one way to define these instructions

patrick91 avatar Sep 11 '24 09:09 patrick91

I think "tasks" works fine as a name, but this is definitely a needed feature. Otherwise we need to fall back on a Makefile or custom Python/bash scripts, which gets needlessly clumsy.

stur86 avatar Sep 19 '24 09:09 stur86

I think "tasks" works fine as a name

For reference, vscode uses the “tasks” nomenclature and they are specified in json.

From https://code.visualstudio.com/docs/editor/tasks

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Run tests",
      "type": "shell",
      "command": "./scripts/test.sh",
      "windows": {
        "command": ".\\scripts\\test.cmd"
      },
      "group": "test",
      "presentation": {
        "reveal": "always",
        "panel": "new"
      }
    }
  ]
}

There are a lot of implementations as others in the thread have pointed out. I am partial to PDM’s approach although I think that it can be greatly simplified. For example, specifying the command type as cmd, call, composite seems too granular and this could just be abstracted from the user. If given a string like uv run ruff check , assume a command. If given a list of strings, assume a composite command. If given a python module path and function, assume a function call.

I think that this tasks concept can be aided by the development dependency group concept and should be developed with them in mind. A task is usually a development related action and always relies on dependencies, whether it is an OS built-in such as echo or whether it’s a python library like ruff, so having a way to specify them for a command (similar to the way dependencies can now be specified in comments for standalone python scripts) should be strongly considered. A way to specify a dependency group, platform, and python version for a named command could add a lot of value.

Supporting arbitrary shell syntax could be tricky because it is not portable and attempts to work around it are usually not DRY. Python directly solves the shell problem by being the cross platform scripting language that Bash and Batch are not, and I think that its use should be encouraged over the latter.

chrisrodrigue avatar Sep 19 '24 12:09 chrisrodrigue

There is a related discussion here: https://discuss.python.org/t/a-new-pep-to-specify-dev-scripts-and-or-dev-scripts-providers-in-pyproject-toml/11457

Unsure if there are any formal PEPs on the topic yet.

chrisrodrigue avatar Sep 19 '24 12:09 chrisrodrigue

This is a good example on how I see as a good practice using poetry and poe:

https://github.com/copier-org/copier/blob/ee9918957cb2bb2abd19898336382d90078383c8/pyproject.toml#L79-L101

Kludex avatar Sep 22 '24 12:09 Kludex

To add to @Kludex comment, i have been using poe for ages, and still do with uv. An example from one of my current projects:

[tool.poe.tasks]
pre.cmd = "pre-commit run --all-files"
pre.help = "Run pre-commit checks"

mypy.cmd = "mypy . --strict"
mypy.help = "Run mypy checks"
format.help = "Format code with Ruff"
format.cmd = "ruff format ."
ruff.help = "Run Ruff checks"
ruff.cmd = "ruff check --output-format=concise ."

test.help = "Run tests using Pytest"
test.cmd = "pytest"
"test:watch".cmd = "ptw . --now --clear"
"test:watch".help = "Run tests using Pytest in watch mode"

changelog.cmd = "github-changelog-md"
changelog.help = "Generate a changelog"

Other projects have more entries for mkdocs and other tools.

Poe is quite well used, it seems that using [tool.uv.tasks] and similar task syntax would be elegant.

seapagan avatar Sep 22 '24 19:09 seapagan

I would love to see built-in task management poethepoet; one feature I'd love to see would be first-class workspaces support similar to yarn: (https://yarnpkg.com/cli/workspaces/foreach)

e.g. something like

Top-level toml

[tool.uv.tasks]
check = "uv workspaces foreach check --topological"
build = "uv workspaces foreach build"

or maybe...

[tool.uv.tasks.check]
foreach_workspace = "check"
workspace_order = "topological"

[tool.uv.tasks.build]
foreach_workspace = "build"
workspace_order = "parallel"

Project Toml

[tool.uv.tasks]
check = ["_ruff", "_pyright"]
build = ["_generate_stuff", "_build_library"]
_ruff = "..."
_pyright = "..."
_generate_stuff = "..."
_build_library = "..."

darthtrevino avatar Sep 23 '24 21:09 darthtrevino

This is a blocker when I tried to move from poetry&poe to uv today.

Here is what existing in my pyproject.toml:

[tool.poe.tasks]
git-hooks = { shell = "pre-commit install --install-hooks && pre-commit install --hook-type commit-msg" }
format = [
  {cmd = "autoflake ."},
  {cmd = "black ."},
  {cmd = "isort ."},
]
lint = [
  {cmd = "black --check ."},
  {cmd = "isort --check-only ."},
  {cmd = "flake8 ."},
]
test = [
  {cmd = "pytest . -vv"},
]
test-cov = [
  {cmd = "pytest --version"},
  {cmd = "coverage run -m pytest ."},
  {cmd = "coverage report --show-missing"},
  {cmd = "coverage xml"},
]
build-doc-and-serve = [
  {cmd = "mkdocs build"},
  {cmd = "mkdocs serve"}
]

Hopefully to see this feature added soon! Thanks

datnguye avatar Sep 28 '24 03:09 datnguye

This is a blocker when I tried to move from poetry&poe to uv today.

Since poe is standalone and works very well with uv its not exactly a 'blocker' (poe is an extra dep even with Poetry) all those scripts will work fine inside your venv, I've recently moved 3 projects from Poetry to uv and everything just works.

It would be nice to have it supported without an extra dependency for sure, though until then just keep using poe🤷‍♂️.

seapagan avatar Sep 28 '24 06:09 seapagan

This is a blocker when I tried to move from poetry&poe to uv today.

Since poe is standalone and works very well with uv its not exactly a 'blocker' (poe is an extra dep even with Poetry) all those scripts will work fine inside your venv, I've recently moved 3 projects from Poetry to uv and everything just works.

It would be nice to have it supported without an extra dependency for sure, though until then just keep using poe🤷‍♂️.

Ah very good point! Totally agreed, thanks for dust 🙏

datnguye avatar Sep 28 '24 11:09 datnguye

Regarding Poe the Poet... I really like how their Poetry plugin allows it to hook into the builtin poetry commands. Their specific example is using a "pre_build" hook to "Optimise static assets for inclusion in the build", which sounds super useful.

harkabeeparolus avatar Oct 03 '24 12:10 harkabeeparolus

I've been trying to adopt thx as a multi-version task runner.

The basic premise is that you configure jobs in pyproject.toml:

[tool.thx]
python_versions = ["3.10", "3.11", "3.12"]
requirements = ["requirements-dev.txt"]

[tool.thx.jobs]
pytest = "pytest --cov"

Then thx pytest mypy builds a venv per interpreter in parallel, installs the package with pinned requirements, and runs pytest for each interpreter.

It also has a --watch mode that stays running and repeats the operation shortly after anything changes.

I like that it is multi-version by default, parallel by default, and the configuration in pyproject.toml is very simple and flat.

lordmauve avatar Oct 04 '24 16:10 lordmauve

For me, it's interesting to run tasks in parallel with a single command (e.g. uv run task start-development). For example, I want to run an MLflow server and execute a FastAPI project in development mode, and if I cancel it (Ctrl + C on the console), both are stopped.

$ uv run mlflow server --host 127.0.0.1 --port 5000

$ fastapi dev src/main.py

Another thing it's interesting, when I have a monorepo with N projects with .NET, the IDE allows me to change, with the UI, the projects I want to run, without overwhelming me, but I don't know what it'd be the equivalent with uv.

AndreuCodina avatar Oct 06 '24 09:10 AndreuCodina