Skip to content
Merged
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
4 changes: 3 additions & 1 deletion TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ git config core.hooksPath .githooks

The version reads as something like `0.1.dev3+g1a2b3c4`. `hatch-vcs` derives the version from git tags, and a new repository has none. The first release creates the first tag, after which the version is ordinary. Do not reintroduce a static `version =` into `pyproject.toml`.

The template requires the published Groundworkers 0.7 stack: `groundworkers[tui]>=0.7,<1` and `oa-configurator>=2,<3`. Import secret annotations such as `Sensitive` and `Secret` from `oa_configurator.refs`. Keep these dependency floors when bootstrapping a new plugin so its wheel resolves to a compatible host and configurator.
The template requires the published Groundworkers 0.10 stack: `groundworkers[tui]>=0.10,<1` and `oa-configurator>=2.1,<3`. Import secret annotations such as `Sensitive` and `Secret` from `oa_configurator.refs`, and the lint types from `groundworkers.plugins` (re-exported by `linting.py`). Keep these dependency floors when bootstrapping a new plugin so its wheel resolves to a compatible host and configurator. A plugin that reads DuckDB vocabularies or DuckDB VSS embeddings also needs the `duckdb` extra: `groundworkers[tui,duckdb]>=0.10,<1`.

Check a configured plugin's settings without starting the server: `groundworkers lint-plugins --config-path /path/to/config.toml` prints a JSON report and exits 1 on any error.

## Improving the template

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description = "Template for a Groundworkers plugin."
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"groundworkers[tui]>=0.7,<1",
"oa-configurator>=2,<3",
"groundworkers[tui]>=0.10,<1",
"oa-configurator>=2.1,<3",
"pydantic>=2,<3",
]

Expand Down
71 changes: 8 additions & 63 deletions src/gw_plugin_template/linting.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,12 @@
"""Groundworkers' shared lint types, with a load-safe fallback for old hosts."""
"""Groundworkers' shared lint types, re-exported for this plugin's modules.

from __future__ import annotations
The host has defined these since Groundworkers 0.8, so they are imported, not copied.
"""

from dataclasses import dataclass
from enum import StrEnum

from groundworkers import plugins as _host_plugin_contract


class PluginLintSeverity(StrEnum):
ERROR = "error"
WARNING = "warning"
INFO = "info"


@dataclass(frozen=True)
class PluginLintDiagnostic:
code: str
severity: PluginLintSeverity
message: str
path: str | None = None

def as_dict(self) -> dict[str, str | None]:
return {
"code": self.code,
"severity": self.severity.value,
"message": self.message,
"path": self.path,
}


@dataclass(frozen=True)
class PluginLintReport:
plugin: str
diagnostics: tuple[PluginLintDiagnostic, ...] = ()

@property
def valid(self) -> bool:
return not any(
item.severity is PluginLintSeverity.ERROR for item in self.diagnostics
)

def as_dict(self) -> dict[str, object]:
return {
"plugin": self.plugin,
"valid": self.valid,
"diagnostics": [item.as_dict() for item in self.diagnostics],
}


# Use one complete host contract when available. Keep the local types together
# on older or partially upgraded hosts so reports never mix incompatible classes.
_LOCAL_LINT_TYPES = (PluginLintSeverity, PluginLintDiagnostic, PluginLintReport)


def _host_lint_types() -> tuple[type, type, type]:
names = ("PluginLintSeverity", "PluginLintDiagnostic", "PluginLintReport")
host_types = tuple(getattr(_host_plugin_contract, name, None) for name in names)
if all(isinstance(item, type) for item in host_types):
return host_types # type: ignore[return-value]
return _LOCAL_LINT_TYPES


PluginLintSeverity, PluginLintDiagnostic, PluginLintReport = _host_lint_types()
from groundworkers.plugins import (
PluginLintDiagnostic,
PluginLintReport,
PluginLintSeverity,
)

__all__ = ["PluginLintDiagnostic", "PluginLintReport", "PluginLintSeverity"]
7 changes: 5 additions & 2 deletions src/gw_plugin_template/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,12 @@ def build(
path = None

# Prefer a resource Groundworkers already resolved. Only resolve your
# own when the plugin genuinely needs a different database.
# own when the plugin genuinely needs a different database. Read the
# shared CDM through `cdm_session_factory`: it routes vocabulary and
# primary roles, including on split-vocabulary stacks, where
# `cdm_engine` cannot reach the vocabulary.
database_source = "Shared Groundworkers CDM database"
available = context.cdm_engine is not None
available = context.cdm_session_factory is not None
if config.catalogue_db is not None:
context.resolver.resolve_database(config.catalogue_db)
database_source = f"Configured database: {config.catalogue_db}"
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class FakeContext:
embedding_backend_factory: object | None = None
embedding_model_backend_factory: object | None = None
chat_backend_factory: object | None = None
cdm_session_factory: object | None = "session-factory"


@pytest.fixture
Expand Down
28 changes: 5 additions & 23 deletions tests/test_plugin_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
from __future__ import annotations

from importlib.metadata import entry_points
from types import SimpleNamespace

from groundworkers import plugins as host_plugin_contract
from groundworkers.plugins import (
GroundworkersPluginConfigUI,
GroundworkersPluginReadiness,
GroundworkersPluginSetup,
PluginLintReport,
lint_plugin_config,
)
from oa_configurator import PackageConfigBase

from gw_plugin_template import linting as linting_contract
from gw_plugin_template.config import GwPluginTemplateConfig
from gw_plugin_template.linting import PluginLintSeverity
from gw_plugin_template.plugin import plugin
Expand Down Expand Up @@ -105,31 +104,14 @@ def test_config_linter_accepts_a_valid_catalogue_snapshot(tmp_path) -> None:
assert report.diagnostics == ()


def test_host_accepts_template_lint_report_when_contract_is_available() -> None:
host_report_type = getattr(host_plugin_contract, "PluginLintReport", None)
host_dispatch = getattr(host_plugin_contract, "lint_plugin_config", None)
if host_report_type is None or host_dispatch is None:
import pytest
def test_host_accepts_template_lint_report() -> None:
report = lint_plugin_config(plugin, GwPluginTemplateConfig())

pytest.skip("Installed Groundworkers does not expose plugin linting yet.")

report = host_dispatch(plugin, GwPluginTemplateConfig())

assert isinstance(report, host_report_type)
assert isinstance(report, PluginLintReport)
assert report.plugin == plugin.name
assert report.valid


def test_partial_host_lint_contract_uses_local_types_as_a_set(monkeypatch) -> None:
monkeypatch.setattr(
linting_contract,
"_host_plugin_contract",
SimpleNamespace(PluginLintReport=object),
)

assert linting_contract._host_lint_types() == linting_contract._LOCAL_LINT_TYPES


def test_build_returns_none_for_foreign_config(fake_context) -> None:
"""`register` is never called when `build` declines."""

Expand Down
Loading
Loading