fix(2-G.5): preview_for_settings contract in adapter docstring + distinguish [] from None

Fixup 1 — Contract section appended to SourceAdapter.preview_for_settings's
docstring. Override authors read adapter.py, not routes.py, so the contract
(pure function of settings; open your own short-lived aiohttp session; None
vs [] semantics) belongs on the base method, not on the GUI stub class.

Fixup 2 — _adapter_preview.html distinguishes [] from None. Previously the
elif test was truthiness (`elif preview_rows`) which collapsed both into
"render nothing". Now uses `elif preview_rows is not none` and special-cases
the empty-list case inside: legend "Preview (0 rows)" with no table; None
still renders nothing at all. Lets adapters signal "query ran, matched zero"
distinctly from "preview not meaningful".

Tests +1:
- test_partial_renders_empty_list — [] yields "Preview (0 rows)" legend,
  no table, no headers. Distinct from the existing None case.

Acceptance:
- 27/27 targeted (preview_hook +1 new, nwis, stream_registry).
- 458/458 full suite.
- (b) framework GUI dir still has zero adapter-name branches.
This commit is contained in:
zvx 2026-05-19 17:55:39 +00:00
commit 570b121276
3 changed files with 26 additions and 1 deletions

View file

@ -85,5 +85,16 @@ class SourceAdapter(ABC):
Return list[dict] (framework renders as a generic table; columns come from
the first dict's keys, in insertion order). Return None to skip preview.
Raise to surface an error banner framework catches at the route boundary.
Contract:
- Preview is a pure function of `settings`. Do NOT access
self._config_store or cursor_db state the framework may instantiate
adapters with a stub config_store solely to call this method.
- Network preview implementations must open their own short-lived
aiohttp session (the adapter's polling session may not exist; the GUI
process never calls startup()).
- Return None when preview is not meaningful (e.g., required settings
like region are unset). Return [] explicitly if the query ran and
matched zero rows the framework renders that distinctly from None.
"""
return None

View file

@ -2,9 +2,10 @@
<article aria-label="Preview Unavailable" style="background-color: var(--pico-del-color); margin-bottom: 1rem;">
<strong>{{ preview_error }}</strong>
</article>
{% elif preview_rows %}
{% elif preview_rows is not none %}
<fieldset>
<legend>Preview ({{ preview_rows|length }} rows)</legend>
{% if preview_rows %}
<table class="preview-table" role="grid">
<thead>
<tr>
@ -19,5 +20,6 @@
{% endfor %}
</tbody>
</table>
{% endif %}
</fieldset>
{% endif %}

View file

@ -106,3 +106,15 @@ def test_partial_renders_nothing_when_both_none():
assert "Preview Unavailable" not in out
# Either empty or only whitespace/newlines from the template.
assert "Preview (" not in out
def test_partial_renders_empty_list():
"""Empty list -> legend with (0 rows), no table.
Distinct from None (which renders nothing at all). Lets adapters signal
'query ran, matched zero rows' separately from 'preview not meaningful'.
"""
out = _render_partial(preview_rows=[], preview_error=None)
assert "Preview (0 rows)" in out
assert "<table" not in out
assert "<th>" not in out