Skip to content
Open
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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,29 @@ jobs:
exit 1
fi

check-manifest-docs:
name: check manifest docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6
with:
environments: "schema"

- name: Regenerate manifest documentation
run: pixi run -e schema generate-manifest-docs

- name: Check if there are any changes
run: |
if ! git diff --quiet; then
echo "Error: Generated manifest documentation differs from committed version"
echo "Please run 'pixi run -e schema generate-manifest-docs' to regenerate and commit."
git diff
exit 1
fi

# Run tests on important platforms.
#

Expand Down
334 changes: 334 additions & 0 deletions docs/reference/_schema_tables.txt

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions docs/reference/pixi_manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ The manifest can be found at the following locations.

## The `workspace` table

--8<-- "docs/reference/_schema_tables.txt:workspace"

The minimally required information in the `workspace` table is:

```toml
Expand Down Expand Up @@ -527,6 +529,8 @@ shared-lib = { path = "packages/shared-lib" }

## The `tasks` table

--8<-- "docs/reference/_schema_tables.txt:tasks"

Tasks are a way to automate certain custom commands in your workspace.
For example, a `lint` or `format` step.
Tasks in a Pixi workspace are essentially cross-platform shell commands, with a unified syntax across platforms.
Expand Down Expand Up @@ -558,13 +562,17 @@ You can modify this table using [`pixi task`](cli/pixi/task.md).

## The `system-requirements` table (deprecated)

--8<-- "docs/reference/_schema_tables.txt:system_requirements"

!!! warning "Deprecated"
The `[system-requirements]` table (and its per-feature variant `[feature.<name>.system-requirements]`) is parsed for backwards compatibility but should not be used in new manifests. Declare the virtual packages directly on [`workspace.platforms`](#inline-table-entries-per-platform-virtual-packages) using inline-table entries.

Existing tables are migrated transparently into synthetic per-platform entries at parse time, and the on-disk file is rewritten the first time you edit platforms through the CLI. See [Migrating from `[system-requirements]`](../workspace/system_requirements.md) for the equivalent forms.

## The `pypi-options` table

--8<-- "docs/reference/_schema_tables.txt:pypi_options"

The `pypi-options` table is used to define options that are specific to PyPI registries.
It can appear in three scopes:

Expand Down Expand Up @@ -781,6 +789,8 @@ skip-wheel-filename-check = true
```

## The `dependencies` table(s)

--8<-- "docs/reference/_schema_tables.txt:dependencies"
??? info "Details regarding the dependencies"
For more detail regarding the dependency types, make sure to check the [Run, Host, Build](../build/dependency_types.md) dependency documentation.

Expand Down Expand Up @@ -1087,6 +1097,8 @@ To help built these dependencies we activate the conda environment that includes
This way when a source distribution depends on `gcc` for example, it's used from the conda environment instead of the system.
## The `activation` table

--8<-- "docs/reference/_schema_tables.txt:activation"

The activation table is used for specialized activation operations that need to be run when the environment is activated.
As with other top level tables, `[activation]` belongs to the `default` feature.
Therefore, every environment that doesn't set `no-default-feature = true` includes that activation script.
Expand Down Expand Up @@ -1137,6 +1149,8 @@ ENV_VAR = "%OTHER_ENV_VAR%\\windows-value"

## The `target` table

--8<-- "docs/reference/_schema_tables.txt:target"

The target table is a table that allows for platform specific configuration.
Allowing you to make different sets of tasks or dependencies per platform.

Expand Down Expand Up @@ -1208,6 +1222,8 @@ This will create an environment called `test` that has `pytest` installed.

### The `feature` table

--8<-- "docs/reference/_schema_tables.txt:feature"

The `feature` table allows you to define the following fields per feature.

- `dependencies`: Same as the [dependencies](#dependencies).
Expand Down Expand Up @@ -1272,6 +1288,8 @@ platforms = ["linux-64-cuda", "osx-arm64"]

### The `environments` table

--8<-- "docs/reference/_schema_tables.txt:environments"

The `[environments]` table allows you to define environments that are created using the features defined in the `[feature]` tables.

The environments table is defined using the following fields:
Expand Down Expand Up @@ -1353,6 +1371,8 @@ More information can be found in the [Dev packages](../build/dev.md) documentati

## The `package` section

--8<-- "docs/reference/_schema_tables.txt:package"

!!! warning "Important note"
`pixi-build` is a [preview feature](#preview-features), and will change until it is stabilized.
Please keep that in mind when you use it for your workspaces.
Expand Down Expand Up @@ -1413,6 +1433,8 @@ And to extend the basics, it can also contain the following fields:

### `build` table

--8<-- "docs/reference/_schema_tables.txt:package_build"

The build system specifies how the package can be built.
The build system is a table that can contain the following fields:

Expand Down
1 change: 1 addition & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ python-fastjsonschema = ">=2.21.2,<3"
pyyaml = ">=6.0.3,<7"

[feature.schema.tasks]
generate-manifest-docs = { cmd = "python generate_manifest_docs.py", cwd = "schema", description = "Generate manifest reference docs from schema model" }
generate-schema = { cmd = "python model.py", cwd = "schema", description = "Generate JSON schema from model" }
test-schema = { cmd = "pytest -s", depends-on = "generate-schema", cwd = "schema", description = "Test the manifest JSON schema" }

Expand Down
222 changes: 222 additions & 0 deletions schema/generate_manifest_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""Generate Markdown reference documentation for the ``pixi.toml`` manifest."""

from __future__ import annotations

import json
import sys
from enum import Enum
from pathlib import Path
from typing import Any, Union, get_args, get_origin

from pydantic import fields as pydantic_fields

from model import (
Activation,
BaseManifest,
Build,
BuildBackend,
ChannelInlineTable,
Environment,
Feature,
MatchspecTable,
Package,
PyPIOptions,
PyPIVersion,
PyPIGitBranchRequirement,
PyPIGitTagRequirement,
PyPIGitRevRequirement,
PyPIPathRequirement,
PyPIUrlRequirement,
S3Options,
SourceLocation,
SystemRequirements,
Target,
TaskInlineTable,
TaskArgs,
DependsOn,
Workspace,
WorkspacePlatform,
hyphenize,
)

HERE = Path(__file__).parent
OUTPUT_PATH = HERE.parent / "docs" / "reference" / "_schema_tables.txt"

AUTOGEN_HEADER = "<!-- This file is autogenerated from schema/model.py. Do not edit manually! -->"

_FRIENDLY: dict[str, str] = {
"NonEmptyStr": "string",
"PositiveFloat": "number",
"AnyHttpUrl": "URL",
"AnyUrl": "URL",
"PathNoBackslash": "path",
"Md5Sum": "md5 string",
"Sha256Sum": "sha256 string",
"ExcludeNewer": "timestamp or duration",
"ChannelName": "string or URL",
"CondaPackageName": "string",
"PyPIPackageName": "string",
"EnvironmentName": "string",
"FeatureName": "string",
"SolveGroupName": "string",
"TargetName": "string",
"TaskName": "string",
"TaskArgName": "string",
"ExtraName": "string",
"FlagName": "string",
"PlatformName": "string",
"Glob": "glob pattern",
"UnsignedInt": "non-negative integer",
"GitUrl": "git URL",
}


def friendly_type(annotation: Any) -> str:
"""Return a human-readable type string for a field annotation."""
if annotation is None:
return "any"

for key, name in _FRIENDLY.items():
if key in str(annotation):
return name

if annotation is type(None):
return "null"

origin = get_origin(annotation)

if isinstance(annotation, type) and issubclass(annotation, Enum):
return " \\| ".join(f"`{e.value}`" for e in annotation)

try:
from typing import Literal

if origin is Literal:
return " \\| ".join(f"`{a}`" for a in get_args(annotation))
except ImportError:
pass

if origin is Union:
args = [a for a in get_args(annotation) if a is not type(None)]
if len(args) == 1:
return friendly_type(args[0])
return " or ".join(friendly_type(a) for a in args)

if origin is list:
args = get_args(annotation)
if args:
return f"list of {friendly_type(args[0])}"
return "list"

if origin is dict:
args = get_args(annotation)
if len(args) == 2:
return f"map of {friendly_type(args[0])} to {friendly_type(args[1])}"
return "map"

if isinstance(annotation, type):
if hasattr(annotation, "model_fields"):
return f"`{annotation.__name__}` object"
return annotation.__name__

if hasattr(annotation, "__metadata__"):
inner = get_args(annotation)
if inner:
return friendly_type(inner[0])

return str(annotation)


def is_required(field_info: pydantic_fields.FieldInfo) -> bool:
if field_info.default is not pydantic_fields.PydanticUndefined:
return False
if field_info.default_factory is not None:
return False
return True


def model_to_table(model_class: Any, skip: set[str] | None = None) -> str:
"""Generate a Markdown table documenting all fields of a Pydantic model."""
skip = skip or set()
lines = [
"| Field | Type | Required | Description |",
"|-------|------|----------|-------------|",
]

for name, info in model_class.model_fields.items():
if name in skip:
continue
display = info.alias or hyphenize(name)
ftype = friendly_type(info.annotation)
req = "yes" if is_required(info) else "no"
desc = (info.description or "").replace("\n", " ").strip()

examples = getattr(info, "examples", None)
if examples:
ex_strs = []
for ex in examples:
if isinstance(ex, (dict, list, bool)):
ex_strs.append(f"`{json.dumps(ex)}`")
else:
ex_strs.append(f"`{ex}`")
if ex_strs:
desc += f"<br>**Examples:** {', '.join(ex_strs)}"

lines.append(f"| `{display}` | {ftype} | {req} | {desc} |")

return "\n".join(lines)


def section(snippet_name: str, model_class: Any, skip: set[str] | None = None) -> str:
"""Generate a snippet with a field table."""
parts = [f"--8<-- [start:{snippet_name}]"]
parts.append(model_to_table(model_class, skip=skip))
parts.append(f"--8<-- [end:{snippet_name}]\n")
return "\n".join(parts)


def generate() -> str:
"""Generate the complete manifest reference snippets."""
out: list[str] = [AUTOGEN_HEADER, ""]

out.append(section("workspace", Workspace, skip={"target"}))
out.append(section("workspace_platforms", WorkspacePlatform))
out.append(section("channel", ChannelInlineTable))
out.append(section("dependencies", MatchspecTable))

for label, cls in [
("pypi_version", PyPIVersion),
("pypi_path", PyPIPathRequirement),
("pypi_url", PyPIUrlRequirement),
("pypi_git_branch", PyPIGitBranchRequirement),
("pypi_git_tag", PyPIGitTagRequirement),
("pypi_git_rev", PyPIGitRevRequirement),
]:
out.append(section(label, cls))

out.append(section("tasks", TaskInlineTable))
out.append(section("task_args", TaskArgs))
out.append(section("task_depends_on", DependsOn))

out.append(section("system_requirements", SystemRequirements))
out.append(section("feature", Feature))
out.append(section("environments", Environment))
out.append(section("activation", Activation))
out.append(section("target", Target))
out.append(section("pypi_options", PyPIOptions))
out.append(section("package", Package))
out.append(section("package_build", Build))
out.append(section("package_build_backend", BuildBackend))
out.append(section("package_build_source", SourceLocation))
out.append(section("s3_options", S3Options))
out.append(section("base_manifest", BaseManifest, skip={"tool"}))

return "\n".join(out) + "\n"


if __name__ == "__main__":
content = generate()
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(content, encoding="utf-8", newline="\n")
kb = round(len(content) / 1024, 2)
print(f"... wrote {kb}kb to {OUTPUT_PATH}", file=sys.stderr)
Loading
Loading