Log: v0.8.0 — PXA Expansion & Hardening Release
Issues closed: #146, #152, #153, #156
Overview
v0.8.0 lands the comprehensive Keysight PXA N9030A driver alongside a hardening pass across the station, transport, logging, and factory layers — plus a Python 3.9 CI restore that had quietly broken the whole test matrix.
New Driver: Comprehensive Keysight PXA N9030A SCPI (#169)
KeysightPXA grew from 2 inherited methods to 29 PXA-specific methods in drivers/keysight.py:
- Measurement config (7) — sweep type, detector, averaging, coupling, impedance
- Advanced triggering (4) — source, level, delay, slope
- Enhanced markers (5) — frequency, position, next peak, threshold, noise
- Bandwidth & sweep (4) — RBW auto, VBW ratio, sweep time, IF gain
- Real-Time Spectrum Analysis (3) — RTSA enable, capture bandwidth, spoiled regions
- System queries (4) — options, serial number, firmware, self-test
Covered by 83 new mock-based tests in tests/test_pxa.py; the 14 pre-existing Keysight tests stay green.
Station & Transport Fixes
Station.connect() idempotent (#179) — closes #156
get_instrument() already connects each driver before Station._add_instrument stores it, so Station.connect() calling inst.connect() again re-opened the VISA resource and re-ran sync_config/_discover_identity/_discover_options. Station.connect() now skips instruments whose connected flag is already set, making repeated connect() calls safe no-ops. Regression tests added in tests/test_station.py and test_adversarial.py.
query_value returns None on failure (#181) — closes #153
VisaDriver.query_value previously returned float 0.0 both when a query raised and when the driver never connected — making a failed read indistinguishable from a genuine 0.0 reading. It now returns None; the success path still returns a stripped str, so a real 0.0 reading survives as "0.0". UUTHandler.mes_voltage handles the new None sentinel explicitly, and the signature is documented as Optional[str]. Four new tests in tests/test_transport_utils.py.
Logging & Diagnostics
_unsupported_feature routes through logging (#178) — closes #152
InstrumentDriver._unsupported_feature() emitted warnings via print(), bypassing logging configuration entirely — applications capturing log records never saw them. All 38 call sites across 7 driver files now route through a module-level logger (drivers/base.py) using logger.warning() with lazy %-formatting. Bonus fix: any falsy model now falls back to 'Instrument' (the old default only fired on a missing key). Regression tests in tests/test_unsupported_feature.py.
- DataBroadcaster send failures are logged —
utils.pylogs failed UDP broadcasts and warns after N consecutive failures instead of failing silently (tests/test_broadcaster.py). - Accurate
UUTHandlerdeprecation notice —device.pynow points to the correct replacement class name.
Configuration
get_config() in config.py now loads JSON/YAML config files with precedence environment variables > config file > built-in defaults. Config files are located via the INSTRUMATION_CONFIG env var, or instrumation.json / instrumation.yaml / instrumation.yml in the current directory or the user's home directory. YAML needs PyYAML (a clear error is raised if missing); JSON is stdlib-only. 110 lines of new tests in tests/test_config.py.
Driver & Factory Hardening
- Real mode fails loudly for unknown driver types (#146) —
get_instrument()raisesValueErrorwhendriver_typeis neither registered nor a canonical category (e.g.SIM), mirroring SIM mode's loud failure, instead of silently falling back toGENERIC. - Abstract contract enforced —
safe_send,query_ascii, andquery_binary_valuesare now abstract onInstrumentDriver, and the async wrappers are validated against the sync signatures (tests/test_async_wrapper_validation.py). check_errorsMock hack removed —RealDriver.check_errorsdetects simulated drivers robustly instead of string-matching a mocked class name (tests/test_check_errors.py).- ASRL port parsing fixed —
factory._skip_serial_probeparses the numeric port from an exactASRLn::INSTRmatch, soASRL10and digits inside TCPIP addresses are no longer misclassified as legacy ports 1-4 (tests/test_serial_probe_filter.py). - Scanner catches same-description conflicts —
find_duplicate_addresseskeys identities on(type, desc), so two genuinely different devices sharing a description string are still reported (tests/test_scanner.py).
CI (Python 3.9 restore) (#182)
config.py:44used PEP 604 union syntax (-> Path | None) which is evaluated at runtime on Python 3.9 and raisedTypeErrorat collection — fail-fast cancelled the rest of the matrix. Fixed withfrom __future__ import annotations(lazy annotations, 3.9-safe, no behavior change).- The abstract-method change exposed that
ReplayDriverwas un-instantiable (TypeError: Can't instantiate abstract class ... query_binary_values). ImplementedReplayDriver.query_binary_valuesreplay-style, mirroringquery_asciiand parsing comma-separated floats — restoring the golden-master tests on every Python version. - Removed the duplicate
.github/workflows/publish-pypi.ymlworkflow; release bumped to v0.8.0.
Test Count
Total: 496 tests passing (up from 458 at the start of this cycle; baseline 492/492 on Python 3.9 and 3.11 after the CI fix).