Skip to content

Fix two plugin-loading bugs: labconfig defaults read as plugin flags, and a setup retry that re-runs side effects - #123

Open
ispielma wants to merge 2 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes
Open

Fix two plugin-loading bugs: labconfig defaults read as plugin flags, and a setup retry that re-runs side effects#123
ispielma wants to merge 2 commits into
labscript-suite:masterfrom
ispielma:UpstreamBugFixes

Conversation

@ispielma

Copy link
Copy Markdown
Contributor

Two independent plugin bugs found while Claude audite labscript-utils and its
callers. Separate commits, both reproduced against master first.

1. Labconfig [DEFAULT] keys are read as plugin enable flags

ConfigParser sections inherit [DEFAULT], so
exp_config.items('BLACS/plugins') returns the section's own options plus
every labconfig default
. Against a typical labconfig:

line 90 sees: ['apparatus_name', 'userlib', 'labscript_suite', 'connection_table']

A plugin directory whose name collides with a [DEFAULT] key therefore never
had an enable flag written into the section, and getboolean() then read the
default's value instead:

ValueError: Not a boolean: /home/u/labscript-suite/userlib

That call is unguarded and the whole block runs at module scope, so
import blacs.plugins raises and BLACS does not start at all, rather than
skipping a single plugin.

The fix writes a real flag into the section when the inherited value is not a
boolean — a section option shadows [DEFAULT], so the plugin resolves from
then on — and swaps the list-membership test for has_option(), which asks the
question directly. An installation that legitimately sets the flag in both
places is unaffected, because the section value already wins:

collision only in [DEFAULT]   before: ValueError   after: starts, plugin disabled
set in both places            before: enabled      after: enabled

2. plugin_setup_complete retry re-runs side effects

The old-API fallback is driven by catching Exception from the call itself, so
any failure inside a correctly-written plugin_setup_complete(data) triggers a
second invocation.

For a plugin whose hook accepts both arities:

def plugin_setup_complete(self, data=None)

the retry re-enters the body, so whatever the first, partially completed call
had already done — started a thread, registered a listener, opened a
connection — happens a second time:

plugin_setup_complete(self, data=None)    before: side effect ran 2x
                                           after: side effect ran 1x

Plugins with a strict arity were never double-run, because the retry failed on
argument count before reaching the body, which is why this has been easy to
miss. Note that labscript_utils.plugins.BasePlugin declares exactly the
optional-argument signature, so this shape is not hypothetical.

The fix binds the signature first and calls once with the arguments it
accepts. Old no-argument plugins still work and still get the deprecation
warning; plugins with no introspectable signature fall back to the current
API. A genuine error now produces one clear log entry instead of a misleading
"Trying again with old call signature" followed by an arity error.


2 files changed, 35 insertions(+), 10 deletions(-)

spielman and others added 2 commits August 28, 2026 14:48
ConfigParser sections inherit [DEFAULT], so items('BLACS/plugins')
returns the section's own options plus every labconfig default. Against a
typical labconfig that means apparatus_name, userlib, labscriptlib,
shared_drive and friends are all seen as configured plugins:

    line 90 sees: ['apparatus_name', 'userlib', 'labscript_suite',
                   'connection_table']

A plugin directory whose name collides with a [DEFAULT] key therefore
never had an enable flag written into the section, and getboolean() then
read the default's value instead:

    ValueError: Not a boolean: /home/u/labscript-suite/userlib

That call is unguarded and this whole block runs at module scope, so
`import blacs.plugins` raised and BLACS did not start at all, rather than
skipping a single plugin.

Write a real flag into the section when the inherited value is not a
boolean; a section option shadows [DEFAULT], so the plugin resolves from
then on. An installation that legitimately sets the flag in both places
is unaffected, since the section value already wins:

    collision only in [DEFAULT]  before: ValueError   after: starts, plugin disabled
    set in both places           before: enabled      after: enabled

Also swap the list-membership test for has_option(), which expresses the
same question directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old-API fallback was driven by catching Exception from the call
itself, so any failure inside a correctly-written
plugin_setup_complete(data) triggered a second invocation.

For a plugin whose hook accepts both arities, such as

    def plugin_setup_complete(self, data=None)

the retry re-enters the body, so whatever the first, partially completed
call had already done - started a thread, registered a listener, opened a
connection - is done a second time:

    plugin_setup_complete(self, data=None)   before: side effect ran 2x
                                              after: side effect ran 1x

Plugins with a strict arity were never double-run, because the retry
failed on argument count before reaching the body, which is why this has
been easy to miss.

Bind the signature first and call once with the arguments it accepts.
Old no-argument plugins still work and still get the deprecation warning;
plugins with no introspectable signature fall back to the current API. A
genuine error now produces one clear log entry rather than a misleading
"Trying again with old call signature" followed by an arity error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant