lsp: declarative action registry + cross-channel language server

Make `testium lsp` (and the testium_assist editor extension that spawns it)
work from every distribution channel: source, wheel, PyInstaller, Flatpak,
AppImage.

Two enablers:

1. Declarative ACTIONS registry. The TestItemActions parents (console, plot,
   json_rpc) now declare their nested actions as a class attribute
   `ACTIONS = {yaml_key: class}`, mirroring PARAMS. The base __init__ seeds
   action_classes from type(self).ACTIONS; register_actions() is kept only as
   an imperative escape hatch. lsp/schema.py reads ACTIONS directly, dropping
   the inspect.getsource/AST walk that returned no actions in a frozen
   PyInstaller build (no .py source on disk).

2. pygls bundled per channel. Kept as the pyproject [lsp] extra (lean
   `pip install testium`), layered into each full-app channel:
   - build_env.sh installs pygls into test/tmp/.venv (source run + PyInstaller
     build env)
   - AppImage installs the wheel as `…whl[lsp]`
   - Flatpak adds a python3-lsp network-pip module (matches the manifest's
     global --share=network)
   - PyInstaller .spec collect_submodules(pygls/lsprotocol) + hiddenimports for
     the lazily-imported lsp/lsp.server/lsp.schema

test/validation/lsp_smoke.py (run by run.sh before the suite) enforces both
per channel: `<channel> schema` must keep console/plot/json_rpc actions and
`<channel> lsp` must answer an initialize request without reporting pygls
missing. Verified for source mode; the other channels need a rebuild to verify.

DESIGN.md updated (declarative section + new "Language server across channels"
subsection + Recent fixes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 23:17:59 +02:00
parent a01268cd0e
commit 8ab53f470d
12 changed files with 204 additions and 58 deletions

View File

@@ -77,7 +77,10 @@ AppDir:
python3.11 -m pip install --break-system-packages --upgrade --isolated --no-input --ignore-installed --prefix=$TARGET_APPDIR/usr -r ../../src/requirements.txt
export PIP_CONFIG_FILE=$HOME/.pip/pip.conf
python3.11 -m pip install --break-system-packages --upgrade --isolated --no-input --ignore-installed --prefix=$TARGET_APPDIR/usr ../../src/dist/testium-{{APP_VERSION}}-py3-none-any.whl
# Install the wheel with the [lsp] extra so `testium lsp` (pygls) works
# from the AppImage. The extra pulls pygls/lsprotocol/cattrs/attrs from
# the index (network is available at build time, see get-pip above).
python3.11 -m pip install --break-system-packages --upgrade --isolated --no-input --ignore-installed --prefix=$TARGET_APPDIR/usr "../../src/dist/testium-{{APP_VERSION}}-py3-none-any.whl[lsp]"
AppImage:

View File

@@ -28,6 +28,20 @@ build-options:
modules:
- python3-requirements.json
# Language-server deps for `testium lsp` (pygls + lsprotocol + cattrs + attrs
# + typing_extensions). Installed from PyPI at build time — the build already
# runs with --share=network (see build-options). The core runtime deps stay
# offline-pinned in python3-requirements.json; these are pure-python wheels,
# hence --only-binary=:all: (no compilation, deterministic).
- name: python3-lsp
buildsystem: simple
build-options:
build-args:
- --share=network
build-commands:
- pip3 install --prefix=${FLATPAK_DEST} --only-binary=:all: "pygls>=1.3"
# 1. Dépendances Python tierces (HORS PySide6)
# Utilisez flatpak-pip-generator pour vos autres libs (ex: pyserial, requests, etc.)
# - name: python3-requirements

View File

@@ -1,5 +1,21 @@
# -*- mode: python ; coding: utf-8 -*-
import os
from PyInstaller.utils.hooks import collect_submodules
# Language-server dependencies for `testium lsp`. pygls/lsprotocol register
# converters and features dynamically, so we collect their submodules wholesale
# and force-import their pure-python deps (cattrs/attrs/typing_extensions).
# The testium lsp modules are imported lazily by the CLI dispatch
# (`from lsp.server import serve`), which PyInstaller's static analysis misses —
# hence the explicit names. No source files need bundling: the schema export is
# now fully declarative (PARAMS + ACTIONS class attributes), so it no longer
# reads .py source via inspect.getsource (which fails in a frozen build).
_LSP_HIDDEN = (
collect_submodules("pygls")
+ collect_submodules("lsprotocol")
+ ["cattrs", "attr", "attrs", "typing_extensions",
"lsp", "lsp.server", "lsp.schema"]
)
# junit_xml is imported by post_exec scripts running under the *host* Python,
# not the frozen interpreter — so bundling it via hiddenimports alone is not
@@ -54,7 +70,7 @@ a = Analysis(
"colorama",
"matplotlib",
"junit_xml",
"lxml"],
"lxml"] + _LSP_HIDDEN,
hookspath=[],
hooksconfig={},
runtime_hooks=[],