Introduce LabeledOption and RangeValues named types

Replace all tuple[str, str] annotations with purpose-specific names:
- LabeledOption = tuple[str, str] for (value, label) pairs used in
  FilterChoice, FilterSelect params, _modifier_options, _find_label,
  and _extract_labeled.
- RangeValues(min, max) NamedTuple for _parse_range return values,
  making the two fields self-documenting at every call site.

Export LabeledOption from common.components alongside SearchSelectOption.
Document the "name compound types explicitly" convention in CLAUDE.md.

https://claude.ai/code/session_01EyAJcMoDktLrY9tSbdHViA
This commit is contained in:
Claude
2026-06-08 18:53:37 +00:00
committed by Lukáš Kucharczyk
parent d9902146dc
commit 11cd62a3b9
4 changed files with 31 additions and 15 deletions
+2
View File
@@ -42,6 +42,7 @@ from common.components.primitives import (
)
from common.components.search_select import (
FilterSelect,
LabeledOption,
SearchSelect,
SearchSelectOption,
searchselect_selected,
@@ -87,6 +88,7 @@ __all__ = [
"PopoverTruncated",
"SearchField",
"FilterSelect",
"LabeledOption",
"SearchSelect",
"SearchSelectOption",
"searchselect_selected",
+18 -11
View File
@@ -8,7 +8,7 @@ from django.utils.safestring import SafeText, mark_safe
from common.components.core import Component
from common.components.primitives import Label, Span
from common.components.search_select import FilterSelect
from common.components.search_select import FilterSelect, LabeledOption
class FilterChoice(NamedTuple):
@@ -19,11 +19,18 @@ class FilterChoice(NamedTuple):
for enum fields the label is resolved from the fixed option list.
"""
selected: list[tuple[str, str]]
excluded: list[tuple[str, str]]
selected: list[LabeledOption]
excluded: list[LabeledOption]
modifier: str
class RangeValues(NamedTuple):
"""A (min, max) string pair parsed from a range filter criterion."""
min: str
max: str
_FILTER_LABEL_CLASS = "text-xs font-medium text-body uppercase tracking-wide"
@@ -55,7 +62,7 @@ def _filter_parse(filter_json: str) -> dict:
return {}
def _extract_labeled(items: list) -> list[tuple[str, str]]:
def _extract_labeled(items: list) -> list[LabeledOption]:
"""Convert a list of bare values or ``{id, label}`` dicts to ``(value, label)`` pairs."""
result = []
for item in items:
@@ -84,12 +91,12 @@ def _filter_get_choice(existing: dict, field: str) -> FilterChoice:
)
def _parse_range(existing: dict, key: str) -> tuple[str, str]:
"""Extract (value, value2) from a filter criterion, defaulting to ("", "")."""
def _parse_range(existing: dict, key: str) -> RangeValues:
"""Extract (min, max) from a range filter criterion, defaulting to ("", "")."""
field = existing.get(key, {})
if not isinstance(field, dict):
return "", ""
return str(field.get("value", "")), str(field.get("value2", ""))
return RangeValues("", "")
return RangeValues(str(field.get("value", "")), str(field.get("value2", "")))
def _parse_bool(existing: dict, key: str) -> bool:
@@ -108,7 +115,7 @@ def _parse_bool(existing: dict, key: str) -> bool:
_FILTER_PREFETCH = 20
def _modifier_options(nullable: bool) -> list[tuple[str, str]]:
def _modifier_options(nullable: bool) -> list[LabeledOption]:
"""Pinned (Any)/(None) pseudo-options; (None) only when the field is nullable."""
options = [("NOT_NULL", "(Any)")]
if nullable:
@@ -618,7 +625,7 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe
def FilterBar(
filter_json: str = "",
status_options: list[tuple[str, str]] | None = None,
status_options: list[LabeledOption] | None = None,
preset_list_url: str = "",
preset_save_url: str = "",
) -> SafeText:
@@ -717,7 +724,7 @@ def FilterBar(
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
def _find_label(options: list[tuple[str, str]], value: str) -> str:
def _find_label(options: list[LabeledOption], value: str) -> str:
for v, label in options:
if str(v) == str(value):
return label
+10 -4
View File
@@ -33,6 +33,12 @@ class SearchSelectOption(TypedDict):
data: dict[str, str] # becomes data-* attrs on the row / pill
# A lightweight (value, label) pair used wherever only those two fields are
# needed — e.g. filter pill lists and modifier pseudo-options. The richer
# SearchSelectOption adds a ``data`` dict for extra row attributes.
LabeledOption = tuple[str, str]
# The pills and the search box share one flex-wrap row (with padding) so the
# widget reads as a single clickable field; the pills wrapper uses `contents`
# so its pills/hidden inputs flow as direct participants of that row, inline
@@ -394,11 +400,11 @@ def _filter_modifier_row(modifier_value: str, label: str) -> SafeText:
def FilterSelect(
*,
field_name: str,
options: list | None = None,
included: list | None = None,
excluded: list | None = None,
options: list[LabeledOption | SearchSelectOption] | None = None,
included: list[LabeledOption | SearchSelectOption] | None = None,
excluded: list[LabeledOption | SearchSelectOption] | None = None,
modifier: str = "",
modifier_options: list[tuple[str, str]] | None = None,
modifier_options: list[LabeledOption] | None = None,
search_url: str = "",
prefetch: int = 0,
items_visible: int = 6,