Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
377825a
initial commit for short circuit composition
prsabahrami Apr 8, 2025
2bf8204
add short-circuit composition tests
prsabahrami Apr 8, 2025
af9319c
fix pre-commit
prsabahrami Apr 8, 2025
e3a2202
refactor: replace macro vec![] with Vec::from()
prsabahrami Apr 8, 2025
34268fb
test: add task5 to short-circuit composition tests and verify output …
prsabahrami Apr 8, 2025
079d09a
chore: update schema
prsabahrami Apr 8, 2025
5099cec
docs: add shorthand syntax for task composition in advanced_tasks.md
prsabahrami Apr 8, 2025
ebb1684
remove string from short circuit definition
prsabahrami Apr 8, 2025
cb45ad2
minor update of the docs
prsabahrami Apr 8, 2025
9910fc5
Update docs/workspace/advanced_tasks.md
prsabahrami Apr 8, 2025
c50448c
docs: remove extra file and paragraph
prsabahrami Apr 8, 2025
49ca2f5
Rearrange and add todos
Hofer-Julian Apr 8, 2025
af7c3e1
feat: update pixi task alias
prsabahrami Apr 8, 2025
7028332
refactor: update pixi.toml snippets and remove todos in advanced_task…
prsabahrami Apr 8, 2025
6c5814e
Merge branch 'main' into short_circuit
prsabahrami Apr 9, 2025
875c491
Doc improvements
Hofer-Julian Apr 9, 2025
09347f6
Update schema
Hofer-Julian Apr 9, 2025
44f3761
Improve test
Hofer-Julian Apr 9, 2025
5910e7a
test: add integration test for pixi task alias command
prsabahrami Apr 9, 2025
2eb36be
minor fix
prsabahrami Apr 9, 2025
582c9db
replace alias with tasks in the test
prsabahrami Apr 9, 2025
3161fc1
fix the tests
prsabahrami Apr 9, 2025
4c1423a
improve test
prsabahrami Apr 9, 2025
114cfb9
do not load the manifest twice in the test
prsabahrami Apr 9, 2025
5a10fe9
fix pre-commit
prsabahrami Apr 9, 2025
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
43 changes: 22 additions & 21 deletions crates/pixi_manifest/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,27 +495,28 @@ impl From<Task> for Item {
Item::Value(Value::InlineTable(table))
}
Task::Alias(alias) => {
let mut table = Table::new().into_inline_table();
table.insert(
"depends-on",
Value::Array(Array::from_iter(alias.depends_on.into_iter().map(|dep| {
match &dep.args {
Some(args) if !args.is_empty() => {
let mut table = Table::new().into_inline_table();
table.insert("task", dep.task_name.to_string().into());
table.insert(
"args",
Value::Array(Array::from_iter(
args.iter().map(|arg| Value::from(arg.clone())),
)),
);
Value::InlineTable(table)
}
_ => Value::from(dep.task_name.to_string()),
}
}))),
);
Item::Value(Value::InlineTable(table))
let mut array = Array::new();
for dep in alias.depends_on.iter() {
let mut table = Table::new().into_inline_table();

table.insert("task", dep.task_name.to_string().into());

if let Some(args) = &dep.args {
table.insert(
"args",
Value::Array(Array::from_iter(
args.iter().map(|arg| Value::from(arg.clone())),
)),
);
}

if let Some(env) = &dep.environment {
table.insert("environment", env.to_string().into());
}

array.push(Value::InlineTable(table));
}
Item::Value(Value::Array(array))
}
_ => Item::None,
}
Expand Down
44 changes: 36 additions & 8 deletions crates/pixi_manifest/src/toml/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,37 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask {
let mut th = match value.take() {
ValueInner::String(str) => return Ok(Task::Plain(str.into_owned()).into()),
ValueInner::Table(table) => TableHelper::from((table, value.span)),
ValueInner::Array(array) => {
let mut deps = Vec::new();
for mut item in array {
match item.take() {
ValueInner::Table(table) => {
let mut th = TableHelper::from((table, item.span));
let name = th.required::<String>("task")?;
let args = th.optional::<Vec<String>>("args");
let environment = th
.optional::<String>("environment")
.map(|env| EnvironmentName::from_str(&env))
.transpose()
.map_err(|e| {
DeserError::from(expected(
"valid environment name",
ValueInner::String(e.attempted_parse.into()),
item.span,
))
})?;

deps.push(Dependency::new(&name, args, environment));
}
_ => return Err(expected("table", item.take(), item.span).into()),
}
}
return Ok(Task::Alias(Alias {
depends_on: deps,
description: None,
})
.into());
}
inner => return Err(expected("string or table", inner, value.span).into()),
};

Expand All @@ -58,7 +89,9 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask {
.map(|mut item| {
let span = item.span;
match item.take() {
ValueInner::String(str) => Ok(Dependency::from(str.as_ref())),
ValueInner::String(str) => Ok::<Dependency, DeserError>(
Dependency::new(str.as_ref(), None, None),
),
ValueInner::Table(table) => {
let mut th = TableHelper::from((table, span));
let name = th.required::<String>("task")?;
Expand All @@ -75,16 +108,13 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask {
))
})?;

// If the creating a new dependency fails, it means the environment name is invalid and exists hence we can safely unwrap the environment
Ok(Dependency::new(&name, args, environment))
}
inner => Err(expected("string or table", inner, span).into()),
}
})
.collect::<Result<Vec<Dependency>, DeserError>>()?,
ValueInner::String(str) => {
vec![Dependency::from(str.as_ref())]
}
ValueInner::String(str) => Vec::from([Dependency::from(str.as_ref())]),
inner => {
return Err::<Vec<Dependency>, DeserError>(
expected("string or array", inner, value.span).into(),
Expand Down Expand Up @@ -129,9 +159,7 @@ impl<'de> toml_span::Deserialize<'de> for TomlTask {
}
})
.collect::<Result<Vec<_>, _>>()?,
ValueInner::String(str) => {
vec![Dependency::from(str.as_ref())]
}
ValueInner::String(str) => Vec::from([Dependency::from(str.as_ref())]),
inner => return Err(expected("string or array", inner, value.span).into()),
};

Expand Down
13 changes: 13 additions & 0 deletions docs/source_files/pixi_tomls/pixi_task_alias.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[workspace]
channels = ["https://prefix.dev/conda-forge"]
name = "pixi"
platforms = ["linux-64", "win-64", "osx-64", "osx-arm64", "linux-aarch64"]

# --8<-- [start:all]
# --8<-- [start:not-all]
[tasks]
fmt = "ruff"
lint = "pylint"
# --8<-- [end:not-all]
style = [{ task = "fmt" }, { task = "lint" }]
# --8<-- [end:all]
19 changes: 14 additions & 5 deletions docs/workspace/advanced_tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,28 @@ pixi task add fmt ruff
pixi task add lint pylint
```

```toml title="pixi.toml"
--8<-- "docs/source_files/pixi_tomls/pixi_task_alias.toml:not-all"
```


### Shorthand Syntax

Pixi supports a shorthand syntax for defining tasks that only depend on other tasks. Instead of using the more verbose `depends-on` field, you can define a task directly as an array of dependencies.

Executing:

```
pixi task alias style fmt lint
```

Results in the following `pixi.toml`.
results in the following `pixi.toml`:

```toml title="pixi.toml"
fmt = "ruff"
lint = "pylint"
style = { depends-on = ["fmt", "lint"] }
--8<-- "docs/source_files/pixi_tomls/pixi_task_alias.toml:all"
```

Now run both tools with one command.
Now you can run both tools with one command.

```shell
pixi run style
Expand Down
2 changes: 1 addition & 1 deletion schema/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ class Target(StrictBaseModel):
pypi_dependencies: dict[PyPIPackageName, PyPIRequirement] | None = Field(
None, description="The PyPI dependencies for this target"
)
tasks: dict[TaskName, TaskInlineTable | NonEmptyStr] | None = Field(
tasks: dict[TaskName, TaskInlineTable | list[DependsOn] | NonEmptyStr] | None = Field(
None, description="The tasks of the target"
)
activation: Activation | None = Field(
Expand Down
6 changes: 6 additions & 0 deletions schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,12 @@
{
"$ref": "#/$defs/TaskInlineTable"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/DependsOn"
}
},
{
"type": "string",
"minLength": 1
Expand Down
45 changes: 45 additions & 0 deletions tests/integration_python/test_main_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1235,3 +1235,48 @@ def test_pixi_task_list_platforms(pixi: Path, tmp_pixi_workspace: Path) -> None:
verify_cli_command(
[pixi, "task", "list", "--manifest-path", manifest], stderr_contains=["foo", "bar"]
)


def test_pixi_add_alias(pixi: Path, tmp_pixi_workspace: Path) -> None:
manifest = tmp_pixi_workspace.joinpath("pixi.toml")
toml = """
[workspace]
name = "test"
channels = []
platforms = ["linux-64", "win-64", "osx-64", "osx-arm64"]
"""
manifest.write_text(toml)

verify_cli_command(
[pixi, "task", "alias", "dummy-a", "dummy-b", "dummy-c", "--manifest-path", manifest]
)
# Test platform-specific task alias
verify_cli_command(
[
pixi,
"task",
"alias",
"--platform",
"linux-64",
"linux-alias",
"dummy-b",
"dummy-c",
"--manifest-path",
manifest,
]
)

with open(manifest, "rb") as f:
manifest_content = tomllib.load(f)

assert "target" in manifest_content
assert "linux-64" in manifest_content["target"]
assert "tasks" in manifest_content["target"]["linux-64"]
assert "linux-alias" in manifest_content["target"]["linux-64"]["tasks"]
assert manifest_content["target"]["linux-64"]["tasks"]["linux-alias"] == [
{"task": "dummy-b"},
{"task": "dummy-c"},
]

assert "dummy-a" in manifest_content["tasks"]
assert manifest_content["tasks"]["dummy-a"] == [{"task": "dummy-b"}, {"task": "dummy-c"}]
38 changes: 38 additions & 0 deletions tests/integration_python/test_run_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,3 +1085,41 @@ def test_multiple_dependencies_with_environments(
"0.2.0",
],
)


def test_short_circuit_composition(pixi: Path, tmp_pixi_workspace: Path) -> None:
"""Test that short-circuiting composition works."""
manifest_path = tmp_pixi_workspace.joinpath("pixi.toml")

manifest_content = tomli.loads(EMPTY_BOILERPLATE_PROJECT)

manifest_content["tasks"] = {
"task1": "echo task1",
"task2": "echo task2",
"task3": [{"task": "task1"}],
"task4": [{"task": "task3"}, {"task": "task2"}],
"task5": {"depends-on": [{"task": "task3"}, {"task": "task2"}]},
}

manifest_path.write_text(tomli_w.dumps(manifest_content))

verify_cli_command(
[pixi, "run", "--manifest-path", manifest_path, "task4"],
stdout_contains=["task1", "task2"],
)

verify_cli_command(
[pixi, "run", "--manifest-path", manifest_path, "task3"],
stdout_contains="task1",
)

output1 = verify_cli_command(
[pixi, "run", "--manifest-path", manifest_path, "task5"],
)

output2 = verify_cli_command(
[pixi, "run", "--manifest-path", manifest_path, "task4"],
)

assert output1.stdout == output2.stdout
assert output1.stderr == output2.stderr
Loading