Skip to content

Contributing and Development

Espen Hovland edited this page Jul 6, 2026 · 4 revisions

Contributing & Development

This page covers working on PyCommander itself: the contribution workflow, setting up a development environment, running the tests, and the layout of the monorepo.

Repository

Contribution workflow

PyCommander is open source and welcomes community patches. The authoritative process lives in the repository's .github/CONTRIBUTING.md; the essentials are summarized here.

  • Open an issue first. Discuss the bug or enhancement before writing code so the change can be scoped and tracked.
  • Fork, don't branch. Direct branching is disabled on the public Silicon Labs repositories — fork the repo and work on a branch in your fork.
  • Branch naming: IssueNumber-short-title, e.g. 99-bootloader-implementation, so work stays traceable to an issue.
  • Signed & signed-off commits are required. Configure a GPG signing key and enable git config commit.gpgsign true. Your user.name (full name) and verified user.email must match your GitHub profile and the email used to sign commits.
  • Commit messages start with IssueNumber-summary-of-changes followed by a detailed description of what changed and why.
  • Sign the CLA. Every new contributor must sign the Silicon Labs Contributor License Agreement during PR review — the 01-CLA-Assistant workflow prompts you to comment your agreement on the PR. Signatures expire after 6 months.
  • Follow the coding standard. Adhere to the Silicon Labs coding guidelines and the conventions below.

Opening a pull request

  1. Fill out the pull request template.
  2. Reviewers are assigned automatically from .github/CODEOWNERS. Add more if needed, but don't remove the code owners.
  3. All CI workflows must pass and total test coverage should remain in the high 90s.
  4. Address review comments; if any are blocking, keep the PR in Draft until they're resolved.

Monorepo layout

The key directories of the monorepo are:

packages/
├── pycommander/                       # meta-package (the `pycommander` command)
├── pycommander-cli/                   # CLI flavor (bundles commander-cli, `pycommander-cli`)
│   └── src/pycommander_cli/_archive/  # bundled Commander CLI archive (fetched, not committed)
├── pycommander-gui/                   # GUI flavor (bundles commander, `pycommander-gui`)
│   └── src/pycommander_gui/_archive/  # bundled Commander GUI archive (fetched, not committed)
└── pycommander-core/                  # the framework (all real logic; no executable)
scripts/
├── download_commander_archives.py     # fetch the Commander archives into the _archive/ dirs
└── run_tests.py                       # aggregate test runner
.github/
└── commander-version                  # pinned Commander release tag used by CI and the script

See Architecture-and-Packages for how the packages relate and how the bundled executable is located and extracted at runtime.

Development install

PyCommander targets Python 3.10+. Install all four packages in editable mode so changes are picked up immediately:

python -m pip install --upgrade pip
python -m pip install -e packages/pycommander-core \
                      -e packages/pycommander-cli \
                      -e packages/pycommander-gui \
                      -e packages/pycommander \
                      --force-reinstall

The core package depends on pyyaml and platformdirs.

Providing Commander executables locally

The flavor packages need a Simplicity Commander archive under their _archive/ directory to actually run hardware commands. Place the platform-appropriate CLI and GUI archives into:

  • packages/pycommander-cli/src/pycommander_cli/_archive/
  • packages/pycommander-gui/src/pycommander_gui/_archive/

The easiest way to populate both directories is the helper script:

python scripts/download_commander_archives.py

With no arguments it reads the Commander release tag pinned in .github/commander-version, auto-detects the host platform, downloads the matching CLI and GUI archives from the GitHub release, and drops them into the two _archive/ directories (clearing out any existing Commander* archives first). This mirrors what the CI workflows do, so a local checkout ends up with the same executables that a released wheel would ship.

The script fails early if either the CLI or GUI archive is missing for the requested tag/platform, so you never end up with a half-populated checkout.

Options:

Flag Default Description
-t, --tag value in .github/commander-version Release tag to download, e.g. commander-1v24p3.
-p, --platform auto Which platform's archives to fetch. auto detects the host; explicit choices are linux-x86_64, linux-aarch64, linux-aarch32, macos, windows.
--repo SiliconLabsSoftware/pycommander GitHub repository (owner/name) to pull the release from.
--token GITHUB_TOKEN / GH_TOKEN env var GitHub token used for private forks. Not needed for the public repo.

Examples:

# Pin a specific Commander release instead of the default:
python scripts/download_commander_archives.py --tag commander-1v24p3

# Fetch a platform other than the host (e.g. grab the Linux build from a Mac):
python scripts/download_commander_archives.py --platform linux-x86_64

# Pull from a private fork (needs a token with access to the release assets):
GITHUB_TOKEN=ghp_xxx python scripts/download_commander_archives.py --repo my-org/pycommander-fork

Note: the archives live as GitHub release assets, not in the git tree, so you need network access (and, for private forks, a token) the first time you set up a checkout. The raw archives are placed as-is; runtime extraction is handled by pycommander_core._ensure_commander.

You can also point the API at a specific executable at runtime via Commander(executable_path=...), which is handy for testing against a particular build without touching _archive/.

Running the tests

Each package has a tests/ suite. The aggregate runner discovers and runs all of them:

python scripts/run_tests.py

To produce coverage like CI does:

python -m pip install coverage
python -m pip install -r packages/*/tests/requirements.txt
coverage run scripts/run_tests.py
coverage report
coverage html        # writes htmlcov/

The test suites use mock runners/adapters/commanders (see packages/pycommander-core/tests/), so most tests do not require real hardware.

Continuous integration

Three GitHub Actions workflows live in .github/workflows/:

  • unittest-and-coverage.yml — runs the test suites and produces a combined coverage report across Linux (x86_64 / aarch64), macOS (arm64) and Windows. Runs on pushes and pull requests targeting main or develop/**, and can also be triggered manually (workflow_dispatch). It downloads the correct Commander archives into the flavor packages before installing and testing. Total coverage is expected to stay in the high 90s.
  • build-wheels-and-publish.yml — builds platform-specific wheels (Linux x86_64/aarch64/aarch32, macOS arm64, Windows) and publishes to PyPI on a v*.*.* tag.
  • 01-CLA-Assistant.yml — enforces the Contributor License Agreement on pull requests (see Contribution workflow below).

Coding conventions

A few patterns to keep in mind when extending the code:

  • One module per CLI suite under pycommander_core/commands/. Each command class subclasses BaseCommand, builds an argument list with the provided helpers (_get_ranges, _get_patches, _get_regions, _get_tokens, _get_include_sections, …), and returns self._run(...).output.
  • CommanderBase auto-registers every command class from commands.__all__, deriving the attribute name from the class name (FlashCommandcommander.flash). When you add a suite, export it from commands/__init__.py and add a type-hint attribute on CommanderBase.
  • High-level helpers (Adapter-Class, Target-Class, AemStreamBase) parse Commander's JSON into the dataclasses in types.py. Validate inputs and raise ValueError/FileNotFoundError early; return None for failed look-ups and bool for actions.
  • All subprocess work goes through runner.Runner, which centralizes timeouts, logging, JSON handling, and the mapping of return codes to typed exceptions.

Adding a new command

  1. Create pycommander_core/commands/<suite>.py with a class <Suite>Command(BaseCommand).
  2. Add public methods that assemble args and call self._run("<suite>", "<subcommand>", *args).output. Document each with a docstring (Args/Returns) — this wiki is generated from those.
  3. Export the class in commands/__init__.py (from .<suite> import <Suite>Command and add to __all__).
  4. Add a type-hint attribute on CommanderBase (<suite> : "<Suite>Command").
  5. (Optional) Add a typed wrapper on Adapter/Target if the command maps to a common task.
  6. Add tests under the relevant package's tests/.

Clone this wiki locally