Packaging a Plugin

This guide explains how to package and distribute a SpectroChemPy plugin.

Project structure

A minimal plugin project looks like this:

spectrochempy-myplugin/
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ README.md
β”œβ”€β”€ LICENSE
β”œβ”€β”€ src/
β”‚   └── myplugin/
β”‚       └── __init__.py      # Plugin class + reader/writer functions
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── test_plugin.py
└── .github/
    └── workflows/
        └── test.yml         # CI configuration (optional)

pyproject.toml

The entry point group spectrochempy.plugins tells SpectroChemPy how to discover your plugin automatically:

[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"

[project]
name = "spectrochempy-myplugin"
version = "0.1.0"
description = "My SpectroChemPy plugin"
requires-python = ">=3.11"
dependencies = [
    "spectrochempy>=0.9.0.dev0,<0.10",
]

[project.entry-points."spectrochempy.plugins"]
myplugin = "myplugin:MyPlugin"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]

Key points:

  • The entry point name (myplugin) must match your plugin’s name attribute.

  • spectrochempy must be listed as a dependency with a compatibility range, e.g. spectrochempy>=0.9.0.dev0,<0.10.

  • Use requires-python = ">=3.11" to match SpectroChemPy’s minimum.

  • Official plugins in the monorepo use a static version field. setuptools_scm is not used because plugin and core tags share the same Git repository, which would cause version collisions.

Local editable development

During local development, install the core package and each plugin in editable mode. This exercises the same entry-point discovery mechanism used by PyPI wheels:

pip install -e .
pip install -e plugins/spectrochempy-nmr
pip install -e plugins/spectrochempy-iris
pip install -e plugins/spectrochempy-cantera

The bundled plugins can also be installed with the helper:

python -m spectrochempy.ci.install_plugins --editable all

After installation, SpectroChemPy discovers the plugins automatically via the spectrochempy.plugins entry point group.

Installing from SpectroChemPy extras

Users can install official plugins through SpectroChemPy extras:

pip install "spectrochempy[nmr]"
pip install "spectrochempy[iris]"
pip install "spectrochempy[plugins]"

The nmr extra installs the NMR plugin, currently including the TopSpin reader and its NMR-specific dependencies. The plugins extra installs the current official plugin set.

Experimental plugins such as spectrochempy-cantera are not included in aggregate extras and must be installed directly:

pip install spectrochempy-cantera

__init__.py β€” the plugin class

Your plugin class must implement the SpectroChemPyPlugin protocol:

from spectrochempy.api.plugins import (
    CORE_PLUGIN_API_VERSION,
    SpectroChemPyPlugin,
)


class MyPlugin(SpectroChemPyPlugin):
    name = "myplugin"
    version = "0.1.0"
    description = "My SpectroChemPy plugin"
    spectrochempy_min_version = "0.9.0.dev0"
    PLUGIN_API_VERSION = CORE_PLUGIN_API_VERSION

    def register_readers(self) -> list[dict]:
        return [...]

See Writing a Plugin for the full API reference.

Entry point discovery flow

  1. User runs import spectrochempy (or calls scp.read_xxx).

  2. PluginManager scans importlib.metadata.entry_points(group="spectrochempy.plugins").

  3. Each entry point is loaded, instantiated, and validated.

  4. Declarative hooks (register_readers(), …) are collected.

  5. Contributions are registered in the plugin registry.

  6. The plugin is marked ACTIVE (or FAILED if an error occurs).

Distribution (PyPI)

Official plugins that still live in the SpectroChemPy monorepo are published from the root workflow:

.github/workflows/publish_plugins.yml

GitHub Actions only executes workflows from the repository root, so workflows stored inside plugins/<plugin>/.github/workflows/ are templates only while the plugin remains in the monorepo. If a plugin is later moved to its own repository, copy the template workflow to that repository’s root .github/workflows/ directory.

The monorepo workflow uses PyPI Trusted Publishing. Each plugin distribution must therefore have its own PyPI/TestPyPI trusted publisher configured with the matching project name, for example spectrochempy-nmr.

For a manual local upload:

pip install build twine
python -m build
twine upload dist/*

Name your package spectrochempy-<name> to make it discoverable.

Release policy

Plugins are released independently from the core package.

  • A core release tag such as 0.9.0 publishes the core wheel only; it does not trigger plugin uploads.

  • A plugin release tag such as spectrochempy-nmr-v0.1.1 triggers the publish_plugins.yml and build_package.yml workflows for that plugin only.

  • The workflow uses skip-existing: true on PyPI so an already-published version never causes a hard failure.

Bumping a plugin version

Plugin versions are declared statically in the monorepo (setuptools-scm is not used for plugins because plugin and core tags share the same Git repository, which leads to version collisions).

The recommended way to release a plugin is through the release_plugin.yml workflow:

  1. Go to Actions β†’ Release an official plugin in the GitHub UI.

  2. Click Run workflow.

  3. Enter: - Plugin name: spectrochempy-nmr - Version: 0.1.1

  4. The workflow bumps pyproject.toml and recipe.yaml, commits, pushes to the plugins branch, and creates the release tag spectrochempy-nmr-v0.1.1 automatically.

  5. The tag triggers CI which builds and publishes the wheel to PyPI and Anaconda.org.

Manual fallback:

# 1. Bump version in plugins/<name>/pyproject.toml
# 2. Bump version in plugins/<name>/recipe.yaml (conda)
# 3. Commit and push:
git add plugins/<name>/pyproject.toml plugins/<name>/recipe.yaml
git commit -m "Bump spectrochempy-<name> to 0.1.1"
git push upstream plugins

# 4. Create and push the release tag:
git tag spectrochempy-<name>-v0.1.1
git push upstream spectrochempy-<name>-v0.1.1

The tag is a pure CI trigger; the actual package version comes from pyproject.toml (pip) and recipe.yaml (conda).

Distribution (conda)

Official plugins with a recipe.yaml in their root are built and published to Anaconda.org automatically by build_package.yml.

  • Dev builds (pushes, PRs) are uploaded to the dev label:

    mamba install -c spectrocat/label/dev -c conda-forge spectrochempy-nmr
    
  • Stable builds (releases) are uploaded to the main label:

    mamba install -c spectrocat -c conda-forge spectrochempy-nmr
    

Plugin recipes should declare a bounded dependency on the core package, e.g. spectrochempy >=0.9.0.dev0,<0.10.

The plugin-template directory is excluded from discovery; it is a developer scaffold and must never be published.

CI / Testing

A recommended CI workflow tests against multiple Python versions:

# .github/workflows/test.yml

jobs:
  test:
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ["3.11", "3.12", "3.13"]  # set via CI matrix
      - name: Install SpectroChemPy
        run: |
          git clone https://github.com/spectrochempy/spectrochempy.git
          pip install -e ./spectrochempy
      - name: Install plugin
        run: pip install -e ".[dev]"
      - name: Run tests
        run: python -m pytest tests/ -v

Use the PluginTestHarness (see Testing a Plugin) for isolated testing without touching the global registry.

See also