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 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. 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. 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 return None

View file

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

View file

@ -106,3 +106,15 @@ def test_partial_renders_nothing_when_both_none():
assert "Preview Unavailable" not in out assert "Preview Unavailable" not in out
# Either empty or only whitespace/newlines from the template. # Either empty or only whitespace/newlines from the template.
assert "Preview (" not in out 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