Command line#

The pyxel-config command line: the ESA-blue wordmark over the command list, with validate, explain, fix, models, params, tutorial, search, index, mcp, doctor and banner

pyxel-config                     # the wordmark, and what it can be asked to do
pyxel-config validate my.yaml    # check it against the bundled schema
pyxel-config validate my.yaml --deep            # and hand it to pyxel.load() too
pyxel-config validate my.yaml --format sarif    # or json / codequality, for CI
pyxel-config explain  my.yaml    # the same checks, with a summary and a next step each
pyxel-config fix      my.yaml -w # repair mistyped and misplaced model names
pyxel-config models charge_generation
pyxel-config params  load_image
pyxel-config search  "dark current"
pyxel-config doctor              # what is installed, bundled, and missing

pyxel-config mcp runs the same server as pyxel-config-mcp, so an assistant and a person are reaching identical code.

The exit code is the verdict, which is what makes this usable in CI without parsing any output:

Code

Meaning

0

valid, or nothing to complain about

1

the config has errors, including text that is not valid YAML or not saved as UTF-8

2

the command line itself was wrong

3

could not run: a file that cannot be opened, YAML that fix cannot read, a missing extra, or --deep that never reached Pyxel’s loader

4

no such tutorial, model or stage, or a search with no hits

A config that cannot be read (a syntax error, a key written twice, a file saved as UTF-16) is a finding in the report like any other, so one broken file never hides the verdict on the rest. Pyxel reads a config file as UTF-8 only, so a file Windows PowerShell 5.1 wrote with > (UTF-16) is an error; its content is still checked.

# .gitlab-ci.yml
validate-configs:
  image: python:3.12-slim
  script:
    - pip install --quiet pyxel-config-core
    # The same verdict as a merge-request widget. The report is written first,
    # and `|| true` keeps a failing verdict from stopping the job before the
    # next line, so the report exists whatever the configs hold.
    - pyxel-config validate configs/*.yaml --format codequality > pyxel-config.json || true
    - pyxel-config validate configs/*.yaml
  artifacts:
    # Uploaded when the job fails too, which is when the widget matters.
    when: always
    reports:
      codequality: pyxel-config.json

Machine-readable reports#

validate and explain take --format {text,json,sarif,codequality}; --json is the short spelling of --format json.

  • json: one entry per config, always a list, with valid, diagnostics, pyxel_version, deep_checked and (for explain) summary.

  • sarif: a SARIF 2.1.0 log with one rule per diagnostic category (pyxel-config/schema, /func, /shape, /advisory, /pyxel-rule, /pyxel-load, /yaml), one result per diagnostic, and the line of the offending key found by walking the YAML. GitHub code scanning, Azure DevOps and most SAST dashboards read it.

  • codequality: the GitLab Code Quality list, with a stable fingerprint per (path, message, file) so a finding is reported once and cleared when it goes away. Root-shape problems, pyxel.load() failures and text that cannot be read as a config are blocker; a bad field is major; a warning is minor.

Both reports name each file relative to the working directory, with forward slashes on every system (SARIF percent-encodes it, and gives a file outside the working directory as a file: URI), so run the check from the repository root and the report points at the files the merge request shows.

The deep check#

The schema is a description of Pyxel; Pyxel is the thing. --deep (or validate_config(config, deep=True)) writes the config to a temporary file and hands it to pyxel.load(), so anything the loader rejects is reported too: a readout with times: [2.0, 1.0] satisfies the schema and the run rules, and only Pyxel knows its times must be strictly increasing. The result names the installed Pyxel whose loader ran (deep_pyxel_version, and “and loads in Pyxel 3.0.2” in the summary), which can differ from the release the bundled schema came from. A warning Pyxel raises while loading is reported as a warning finding; its own deprecation notices, which Python hides from a user too, are left out.

Loading runs part of the config. Pyxel evaluates a readout’s times and an observation’s values as Python when they are written as text, evaluates an APD converter’s function (and unpickles one written as binary data), and imports the module each model’s func names. It also opens a readout’s times_from_file, a custom observation’s from_file and an APD converter’s filename, which can be URLs. So before it loads anything, the deep check reads every such value and refuses to run the config unless each is plain data or a known harmless call: a list of numbers, numpy.linspace/arange/logspace/geomspace or range() with plain numbers and at most a million values, a one-argument lambda of arithmetic and math functions, and model functions under pyxel.. Binary data (YAML !!binary) and files to open are always refused. A refusal is a warning that names the value and why. For a config you wrote yourself, --trust-config (or trusted=True) skips that reading. Never use it on a config from someone else, such as a merge request from a fork.

On the MCP server the deep check is off: an assistant can pass in a config from an issue, an email or a web page. Whoever starts the server can allow it with pyxel-config mcp --allow-deep, and the refusal rules above still apply.

When the loader cannot run (Pyxel not installed, or installed but failing to import, which the warning names), deep_checked stays false and the command line exits with 3, “could not run”, rather than 0. A config the schema already rejects still exits with 1.

As a pre-commit hook#

The repository exposes a pyxel-config-validate hook, so staged YAML configs are checked before the commit lands. By default it checks every YAML file except the CI and tooling files that are never Pyxel configs (.gitlab-ci.yml, GitHub workflows, pre-commit’s own files, docker-compose, mkdocs, conda environments, Read the Docs, codecov, yamllint); narrowing files to where the configs live is still the surest choice:

# .pre-commit-config.yaml
repos:
  - repo: https://gitlab.com/dobybaxter127/pyxel-config-lab
    rev: core-v0.6.1
    hooks:
      - id: pyxel-config-validate
        files: ^configs/.*\.ya?ml$   # narrow it to where the configs live

stdout carries the answer, stderr carries the furniture. The banner and any progress note go to stderr, so a pipe stays clean with no flag at all:

pyxel-config models --json | jq -r '.models[].func'
pyxel-config fix my.yaml -o - > repaired.yaml     # config out, report on stderr

Colour follows ESA: institutional blue and white carry the frame and the labels, and red and amber are kept for the things that are actually wrong. Palette entries are named for the job they do, not the hue, so a repaint never reaches a call site. It steps down on its own: truecolor to 256 to 16 to none, Unicode glyphs to ASCII when the output encoding cannot carry them, and a wordmark that shrinks through two smaller lockups rather than wrapping. --json, --no-color, --ascii, --banner and -q override any of that; NO_COLOR is honoured.