From 6f081abf375a22c1aeeee00fc8b2bbc84ec992c1 Mon Sep 17 00:00:00 2001 From: Simon Clark Date: Mon, 4 May 2026 15:41:05 +0100 Subject: [PATCH 1/2] docs: add testing patterns (pytest mocks + TestingBase) and Python 3.13 deprecation notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fills two gaps in the skill after the 1.9.0 docs pass: - New patterns/testing.md covers Pattern A (unit tests with mocked indigo via unittest.mock) and Pattern B (live integration with TestingBase submodule + APIBase + ValidateXmlFile). Documents both as complementary, with a side-by-side comparison and setup instructions. - Python3-Migration-Guide.md gains an "Indigo 2025.2 / Python 3.11 → 3.13" section covering generic 3.13 deprecations not specific to a particular library: datetime.utcnow, asyncio.get_event_loop, pkg_resources, the PEP 594 stdlib removals (cgi/telnetlib/etc.), and the imp/distutils removals from 3.12. Cross-links to the existing troubleshooting section for library-specific breakages. - /indigo:dev routing (commands/dev.md + skills/dev/SKILL.md) now surfaces the testing doc on prompts mentioning "test", "pytest", "mocking", or "TestingBase". - Bump plugin.json + marketplace.json to 1.10.0 (minor — new docs surface). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- commands/dev.md | 8 +- docs/plugin-dev/patterns/testing.md | 128 +++++++++++++++++++++++++++ reference/Python3-Migration-Guide.md | 53 +++++++++++ skills/dev/SKILL.md | 1 + 6 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 docs/plugin-dev/patterns/testing.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ccba5f3..ea164ce 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "name": "indigo", "source": "./", "description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building", - "version": "1.9.0", + "version": "1.10.0", "repository": "https://github.com/simons-plugins/indigo-claude-plugin", "license": "MIT", "keywords": [ diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 405903b..3deb793 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "indigo", - "version": "1.9.0", + "version": "1.10.0", "description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building", "repository": "https://github.com/simons-plugins/indigo-claude-plugin" } diff --git a/commands/dev.md b/commands/dev.md index ad23a53..5319202 100644 --- a/commands/dev.md +++ b/commands/dev.md @@ -30,6 +30,7 @@ Expert assistant for Indigo home automation plugin development. Provides compreh | `docs/plugin-dev/troubleshooting/common-issues.md` | 11KB | Troubleshooting | | `docs/plugin-dev/patterns/api-patterns.md` | 5KB | Common API patterns | | `docs/plugin-dev/patterns/open-source-contributing.md` | 3KB | Contributing to IndigoDomotics open source | +| `docs/plugin-dev/patterns/testing.md` | 4KB | Testing — pytest mocks (Pattern A) and TestingBase live integration (Pattern B) | ### Modular IOM Reference (Load by Topic) @@ -70,6 +71,7 @@ These are complementary - load based on question type: | Subscriptions | `docs/plugin-dev/api/iom/subscriptions.md` | | Schedule/action group commands | `docs/plugin-dev/api/iom/command-namespaces.md` | | Open source contributing | `docs/plugin-dev/patterns/open-source-contributing.md` | +| Testing, pytest, mocking, TestingBase | `docs/plugin-dev/patterns/testing.md` | ### Specific Routing @@ -99,6 +101,9 @@ These are complementary - load based on question type: **"How do I contribute to open source?" / "Add plugin to IndigoDomotics"** 1. Read `docs/plugin-dev/patterns/open-source-contributing.md` +**"How do I test my plugin?" / "Write tests" / "TestingBase" / "pytest"** +1. Read `docs/plugin-dev/patterns/testing.md` + **"What device properties exist?"** 1. Read `docs/plugin-dev/api/iom/devices.md` @@ -143,7 +148,8 @@ docs/plugin-dev/ │ └── utilities.md # Helpers (4KB) ├── patterns/ │ ├── api-patterns.md # Common patterns (5KB) -│ └── open-source-contributing.md # IndigoDomotics contributing guide (3KB) +│ ├── open-source-contributing.md # IndigoDomotics contributing guide (3KB) +│ └── testing.md # pytest mocks + TestingBase (4KB) ├── examples/ │ └── sdk-examples-guide.md # Example catalog (8KB) └── troubleshooting/ diff --git a/docs/plugin-dev/patterns/testing.md b/docs/plugin-dev/patterns/testing.md new file mode 100644 index 0000000..889442c --- /dev/null +++ b/docs/plugin-dev/patterns/testing.md @@ -0,0 +1,128 @@ +# Testing Patterns + +How to test Indigo plugins. Two complementary patterns: + +- **Pattern A — Unit tests with mocks**: fast, no Indigo runtime required, runs in CI. +- **Pattern B — Live integration with TestingBase**: exercises a real running Indigo server, catches integration bugs and validates plugin XML. + +They are complementary, not alternatives. Run A often, B before a release. + +--- + +## Pattern A — Unit tests with a mocked Indigo runtime + +Mock `indigo` via `unittest.mock` and import your plugin module directly. Tests can then exercise business logic — API parsing, state computation, ConfigUI validation, scheduling — without an Indigo install. + +**When to use**: anything that doesn't depend on Indigo's database — most of your plugin's logic. + +**Reference shape**: + +```python +# tests/conftest.py +import sys +from pathlib import Path +from unittest.mock import Mock +import pytest + +SERVER_PLUGIN_DIR = ( + Path(__file__).parent.parent + / "MyPlugin.indigoPlugin" + / "Contents" + / "Server Plugin" +) +sys.path.insert(0, str(SERVER_PLUGIN_DIR)) + + +@pytest.fixture +def mock_logger(): + logger = Mock() + for level in ("debug", "info", "warning", "error", "exception"): + setattr(logger, level, Mock()) + return logger +``` + +Tests then import plugin modules and inject `mock_logger` (and other fixtures) where the plugin would normally use `self.logger`. Run with `pytest` — no Indigo running, no credentials, no network. + +--- + +## Pattern B — Live integration with TestingBase + +Indigo ships [`TestingBase`](https://github.com/IndigoDomotics/TestingBase) as a shared git submodule. Tests subclass `APIBase` (a `unittest.TestCase`) and exercise a running Indigo server's HTTP API. The companion `ValidateXmlFile` helper validates `Devices.xml`, `Actions.xml`, `Events.xml`, and `MenuItems.xml` against the Indigo schema. + +**When to use**: pre-release smoke tests; XML schema validation; end-to-end checks against your plugin's HTTP responder. + +**Setup**: + +```bash +# At the top level of your plugin repo +git submodule add https://github.com/IndigoDomotics/TestingBase.git tests/shared +git submodule update --init +``` + +**Layout** (the convention TestingBase expects): + +``` +my-plugin/ +├── tests/ +│ ├── shared/ # submodule — do NOT edit +│ ├── .env # gitignored — Indigo API credentials +│ ├── testing-requirements.txt # references shared/module-requirements.txt +│ ├── venv/ # gitignored — your test venv +│ └── test_my_plugin.py +``` + +`tests/.env` carries credentials — see `tests/shared/ENV_TEMPLATE` for the exact keys. `tests/testing-requirements.txt` should chain to the shared list: + +``` +-r shared/module-requirements.txt +# anything else your tests need +``` + +**Minimal `APIBase` test**: + +```python +# tests/test_my_plugin.py +from shared import APIBase + +class TestMyPlugin(APIBase): + def test_device_reachable(self): + device = self.get_indigo_object() + self.assertTrue(device["enabled"]) +``` + +**XML validation** — `ValidateXmlFile` MUST come first in the MRO: + +```python +from shared import APIBase, ValidateXmlFile + +class TestActionsXml(ValidateXmlFile, APIBase): + server_plugin_dir_path = ( + "/path/to/MyPlugin.indigoPlugin/Contents/Server Plugin" + ) + file_name = "Actions.xml" +``` + +**Maintenance**: `tests/shared` is a submodule. Pull updates with +`git submodule update --recursive --remote tests/shared` and never commit +local changes back to it — the upstream README is explicit about that. + +--- + +## Choosing between A and B + +| | Pattern A (mocks) | Pattern B (TestingBase) | +|---|---|---| +| Speed | Seconds | Slow (spawns processes per call) | +| Indigo install needed | No | Yes — running server + admin API access | +| What it catches | Logic errors | Integration + XML schema errors | +| Best for | CI on every PR | Pre-release smoke | + +Use both for any non-trivial plugin: A on every commit, B before each release. + +--- + +## References + +- TestingBase upstream: https://github.com/IndigoDomotics/TestingBase +- Plugin HTTP API (consumed by Pattern B): see `/indigo:api` +- Plugin lifecycle (what you'd typically test): see `concepts/plugin-lifecycle.md` diff --git a/reference/Python3-Migration-Guide.md b/reference/Python3-Migration-Guide.md index c7104ee..a5fbf69 100644 --- a/reference/Python3-Migration-Guide.md +++ b/reference/Python3-Migration-Guide.md @@ -124,3 +124,56 @@ - `dict.iteritems()` is deprecated, `dict.items()` works just as well in both python 2 & 3 (same for all the iter* functions on dict and list objects). - `pylint -py3k` may give you this error: `round built-in referenced (round-builtin)`. This warning can safely be ignored since it's primarily just a reminder that the [algorithm for calculating how an exact halfway cases](https://docs.python.org/3/whatsnew/3.0.html#builtins) has changed in python 3. + +--- + +## Indigo 2025.2 / Python 3.11 → 3.13 + +Indigo 2025.2 bumps the embedded interpreter from Python **3.11 to 3.13.9**. The 3.11→3.13 step is small compared to 2→3, and most plugin code needs no changes. Two categories warrant a quick sweep before upgrading. + +### Library-level breakages + +For specific third-party library and stdlib import errors observed under 3.13 (`telnetlib`, `websockets` v14+, `matplotlib`'s `legendHandles` and `plot_date`), see [common-issues.md → Upgrading to Indigo 2025.2 / Python 3.13](../docs/plugin-dev/troubleshooting/common-issues.md#upgrading-to-indigo-20252--python-313). Those are the issues seen in the wild; the rest of this section covers generic 3.13 deprecations to clean up while you're in the area. + +### Deprecations to clean up + +These don't fail under 3.13 yet but are slated for removal: + +- **`datetime.utcnow()`** — deprecated since 3.12, returns naive datetimes. + ```python + # before + ts = datetime.utcnow() + # after + from datetime import datetime, UTC + ts = datetime.now(UTC) + ``` + +- **`asyncio.get_event_loop()`** with no running loop emits a DeprecationWarning since 3.10. + ```python + # inside a coroutine + loop = asyncio.get_running_loop() + # creating a new loop explicitly + loop = asyncio.new_event_loop() + ``` + +- **`pkg_resources`** is deprecated. Prefer `importlib.metadata` for distribution introspection. + +### Removed stdlib (PEP 594 — completed in 3.13) + +If any of these appear in your imports, they will now fail at import time: + +`aifc`, `audioop`, `cgi`, `chunk`, `crypt`, `imghdr`, `mailcap`, `nis`, `nntplib`, `ossaudiodev`, `pipes`, `sndhdr`, `spwd`, `sunau`, `telnetlib`, `uu`, `xdrlib`. + +For `telnetlib` specifically, the `telnetlib-313-and-up` PyPI shim restores the same import path — see the troubleshooting note linked above. + +`imp` (removed in 3.12) and `distutils` (removed in 3.12) are gone too if you've been carrying any pre-3.12 code. Switch to `importlib` and `setuptools`/`packaging`. + +### Vendored packages + +Pre-built packages in `Contents/Packages/` are tagged with the Python version that created them. Pure-Python wheels (`.py` only — no `.so` files) usually transport across versions. Anything with compiled extensions needs rebuilding for 3.13. The safer pattern is `requirements.txt` — Indigo 2025.2 will reinstall under 3.13 automatically and writes a version-keyed `3.13-pip-install-log-success.txt` marker. + +### Testing under 3.13 + +Run your existing test suite (Pattern A from [patterns/testing.md](../docs/plugin-dev/patterns/testing.md)) under 3.13 locally before installing 2025.2 on a live server. If you don't have a test suite, restart plugins one at a time after the upgrade and watch the Indigo Event Log for tracebacks — bump `PluginVersion` and ship a fix for any plugin that fails to start. + +To roll back to 2025.1: quit 2025.2 server and client, relaunch 2025.1. diff --git a/skills/dev/SKILL.md b/skills/dev/SKILL.md index 5c7435c..f620c72 100644 --- a/skills/dev/SKILL.md +++ b/skills/dev/SKILL.md @@ -122,6 +122,7 @@ For detailed guidance on specific topics, read these files relative to `${CLAUDE | Scripting shell & CLI | `docs/plugin-dev/concepts/scripting-shell.md` | | Plugin preferences & PluginConfig.xml | `docs/plugin-dev/concepts/plugin-preferences.md` | | API patterns (state updates, replaceOnServer) | `docs/plugin-dev/patterns/api-patterns.md` | +| Testing patterns (pytest mocks, TestingBase) | `docs/plugin-dev/patterns/testing.md` | | Troubleshooting | `docs/plugin-dev/troubleshooting/common-issues.md` | | SDK examples guide | `docs/plugin-dev/examples/sdk-examples-guide.md` | | Indigo Object Model overview | `docs/plugin-dev/api/indigo-object-model.md` | From 8564a97f90297264b1a462d07c752c7df55e0179 Mon Sep 17 00:00:00 2001 From: Simon Clark Date: Mon, 4 May 2026 16:15:24 +0100 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20address=20review=20=E2=80=94=20fix?= =?UTF-8?q?=20asyncio/PEP594=20facts,=20ValidateXmlFile=20path=20idiom,=20?= =?UTF-8?q?downgrade=20to=201.9.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on PR #38: - reference/Python3-Migration-Guide.md: asyncio.get_event_loop() DeprecationWarning was added in 3.12 (cpython gh-100160), not 3.10. Fixed. - reference/Python3-Migration-Guide.md: PEP 594 list was missing cgitb and msilib (verified against the 3.13 What's New). Added. - reference/Python3-Migration-Guide.md: dropped unverified specific patch claim "3.13.9" to plain "3.13". - docs/plugin-dev/patterns/testing.md: ValidateXmlFile example used a hardcoded absolute path which fails across machines. Switched to upstream's os.path-relative idiom. - docs/plugin-dev/patterns/testing.md: clarified APIBase is abstract (not just any TestCase) so readers don't try to instantiate it. - docs/plugin-dev/patterns/testing.md: refined "Slow (spawns processes per call)" — only run_host_script-based helpers spawn IPH3, not every assertion. Plain HTTP calls are just round-trips. - docs/plugin-dev/patterns/testing.md: added one-line clarifying comment to the mock_logger fixture explaining the explicit assigns are for readability (Mock auto-creates attrs on access). - docs/plugin-dev/patterns/README.md: added Testing entry to the index — was the only discoverability surface missed in the initial PR. - .claude-plugin/{plugin,marketplace}.json: scope-correct version bump 1.10.0 → 1.9.1. Pure docs additions are patch territory per the workspace memory rule (minor for user-visible features, patch for internal/reliability work). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- docs/plugin-dev/patterns/README.md | 5 +++++ docs/plugin-dev/patterns/testing.md | 16 +++++++++++----- reference/Python3-Migration-Guide.md | 6 +++--- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ea164ce..d2912ae 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "name": "indigo", "source": "./", "description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building", - "version": "1.10.0", + "version": "1.9.1", "repository": "https://github.com/simons-plugins/indigo-claude-plugin", "license": "MIT", "keywords": [ diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3deb793..3a10393 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "indigo", - "version": "1.10.0", + "version": "1.9.1", "description": "Indigo home automation development toolkit \u2014 plugin development, API integration, and control page building", "repository": "https://github.com/simons-plugins/indigo-claude-plugin" } diff --git a/docs/plugin-dev/patterns/README.md b/docs/plugin-dev/patterns/README.md index b407bb8..3fdf560 100644 --- a/docs/plugin-dev/patterns/README.md +++ b/docs/plugin-dev/patterns/README.md @@ -12,6 +12,11 @@ Core patterns for working with the Indigo Object Model: - Variable and action group patterns - Common anti-patterns to avoid +### [Testing](testing.md) +Two complementary testing patterns: +- Pattern A — `unittest.mock`-based unit tests (fast, no Indigo runtime, runs in CI) +- Pattern B — live integration tests with the upstream `TestingBase` git submodule (`APIBase` + `ValidateXmlFile` against a running server) + ## Coming Soon We're building additional implementation patterns including: diff --git a/docs/plugin-dev/patterns/testing.md b/docs/plugin-dev/patterns/testing.md index 889442c..7d1825c 100644 --- a/docs/plugin-dev/patterns/testing.md +++ b/docs/plugin-dev/patterns/testing.md @@ -35,6 +35,8 @@ sys.path.insert(0, str(SERVER_PLUGIN_DIR)) @pytest.fixture def mock_logger(): + # Mock() auto-creates these attributes on first access — the explicit + # assignment is for readability so test failures point at named mocks. logger = Mock() for level in ("debug", "info", "warning", "error", "exception"): setattr(logger, level, Mock()) @@ -47,7 +49,7 @@ Tests then import plugin modules and inject `mock_logger` (and other fixtures) w ## Pattern B — Live integration with TestingBase -Indigo ships [`TestingBase`](https://github.com/IndigoDomotics/TestingBase) as a shared git submodule. Tests subclass `APIBase` (a `unittest.TestCase`) and exercise a running Indigo server's HTTP API. The companion `ValidateXmlFile` helper validates `Devices.xml`, `Actions.xml`, `Events.xml`, and `MenuItems.xml` against the Indigo schema. +Indigo ships [`TestingBase`](https://github.com/IndigoDomotics/TestingBase) as a shared git submodule. Tests subclass `APIBase` — an abstract `unittest.TestCase` — and exercise a running Indigo server's HTTP API. The companion `ValidateXmlFile` helper validates `Devices.xml`, `Actions.xml`, `Events.xml`, and `MenuItems.xml` against the Indigo schema. **When to use**: pre-release smoke tests; XML schema validation; end-to-end checks against your plugin's HTTP responder. @@ -90,14 +92,18 @@ class TestMyPlugin(APIBase): self.assertTrue(device["enabled"]) ``` -**XML validation** — `ValidateXmlFile` MUST come first in the MRO: +**XML validation** — `ValidateXmlFile` MUST come first in the MRO. Resolve the path relative to the test file so the test runs on any machine: ```python +import os from shared import APIBase, ValidateXmlFile class TestActionsXml(ValidateXmlFile, APIBase): - server_plugin_dir_path = ( - "/path/to/MyPlugin.indigoPlugin/Contents/Server Plugin" + server_plugin_dir_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "../MyPlugin.indigoPlugin/Contents/Server Plugin", + ) ) file_name = "Actions.xml" ``` @@ -112,7 +118,7 @@ local changes back to it — the upstream README is explicit about that. | | Pattern A (mocks) | Pattern B (TestingBase) | |---|---|---| -| Speed | Seconds | Slow (spawns processes per call) | +| Speed | Seconds | Slower — HTTP round-trips per assertion; helpers like `run_host_script` spawn an IPH3 process per call | | Indigo install needed | No | Yes — running server + admin API access | | What it catches | Logic errors | Integration + XML schema errors | | Best for | CI on every PR | Pre-release smoke | diff --git a/reference/Python3-Migration-Guide.md b/reference/Python3-Migration-Guide.md index a5fbf69..3e3a70b 100644 --- a/reference/Python3-Migration-Guide.md +++ b/reference/Python3-Migration-Guide.md @@ -129,7 +129,7 @@ ## Indigo 2025.2 / Python 3.11 → 3.13 -Indigo 2025.2 bumps the embedded interpreter from Python **3.11 to 3.13.9**. The 3.11→3.13 step is small compared to 2→3, and most plugin code needs no changes. Two categories warrant a quick sweep before upgrading. +Indigo 2025.2 bumps the embedded interpreter from Python **3.11 to 3.13**. The 3.11→3.13 step is small compared to 2→3, and most plugin code needs no changes. Two categories warrant a quick sweep before upgrading. ### Library-level breakages @@ -148,7 +148,7 @@ These don't fail under 3.13 yet but are slated for removal: ts = datetime.now(UTC) ``` -- **`asyncio.get_event_loop()`** with no running loop emits a DeprecationWarning since 3.10. +- **`asyncio.get_event_loop()`** with no running loop emits a `DeprecationWarning` since 3.12 (cpython gh-100160) and will be removed in 3.14. Inside a coroutine, use `asyncio.get_running_loop()` instead. ```python # inside a coroutine loop = asyncio.get_running_loop() @@ -162,7 +162,7 @@ These don't fail under 3.13 yet but are slated for removal: If any of these appear in your imports, they will now fail at import time: -`aifc`, `audioop`, `cgi`, `chunk`, `crypt`, `imghdr`, `mailcap`, `nis`, `nntplib`, `ossaudiodev`, `pipes`, `sndhdr`, `spwd`, `sunau`, `telnetlib`, `uu`, `xdrlib`. +`aifc`, `audioop`, `cgi`, `cgitb`, `chunk`, `crypt`, `imghdr`, `mailcap`, `msilib`, `nis`, `nntplib`, `ossaudiodev`, `pipes`, `sndhdr`, `spwd`, `sunau`, `telnetlib`, `uu`, `xdrlib`. For `telnetlib` specifically, the `telnetlib-313-and-up` PyPI shim restores the same import path — see the troubleshooting note linked above.