diff --git a/src/formbridge/forms/ui_utils.py b/src/formbridge/forms/ui_utils.py index a61ac5b..09cef82 100644 --- a/src/formbridge/forms/ui_utils.py +++ b/src/formbridge/forms/ui_utils.py @@ -5,6 +5,7 @@ """ import logging +import re from dataclasses import dataclass from enum import Enum from typing import Any @@ -21,7 +22,13 @@ class FieldDisplayText: @classmethod def from_field_name(cls, name: str) -> "FieldDisplayText": - display_name = name.replace("_", " ").title() + # Labels accept both parameter_names and nominal ClassNames. + words = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", name) + words = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", words) + display_name = " ".join( + word if word.isupper() else word.title() + for word in words.replace("_", " ").split(" ") + ) return cls( display_name=display_name, field_label=f"{display_name}:", diff --git a/src/formbridge/utils/preview_formatters.py b/src/formbridge/utils/preview_formatters.py index b0a49b4..9382f9e 100644 --- a/src/formbridge/utils/preview_formatters.py +++ b/src/formbridge/utils/preview_formatters.py @@ -9,6 +9,8 @@ from objectstate import ParameterOwner +from formbridge.forms.ui_utils import FieldDisplayText + @dataclass(frozen=True, slots=True) class PreviewLabelResolution: @@ -95,7 +97,7 @@ def format_preview_value( value: object, detail: PreviewValueDetail = PreviewValueDetail.COMPACT ) -> str | None: """Project display values by nominal type, without knowing config field names.""" - if callable(value) and not isinstance(value, type): + if callable(value): return type(value).__name__ return str(value) @@ -110,6 +112,12 @@ def _format_enum(value: Enum, detail=PreviewValueDetail.COMPACT) -> str | None: return value.name if value.value is not None else None +@format_preview_value.register(type) +def _format_type(value: type, detail=PreviewValueDetail.COMPACT) -> str: + declaration = canonical_declaration_mro(value)[0] + return FieldDisplayText.from_field_name(declaration.__name__).display_name + + @format_preview_value.register(FunctionType) @format_preview_value.register(MethodType) def _format_function(value, detail=PreviewValueDetail.COMPACT) -> str: diff --git a/tests/test_preview_value_names.py b/tests/test_preview_value_names.py new file mode 100644 index 0000000..f34a5d4 --- /dev/null +++ b/tests/test_preview_value_names.py @@ -0,0 +1,55 @@ +"""Class-valued choices have readable captions without changing their identity.""" + +from abc import ABCMeta +from dataclasses import dataclass + +import pytest +from objectstate.lazy_factory import LazyDataclassFactory + +from formbridge.forms.ui_utils import FieldDisplayText +from formbridge.managers.preview_formatting import ManagerPreviewFieldFormatter +from formbridge.utils.preview_formatters import PreviewFieldFormatRequest, format_preview_value + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("read_previous_step", "Read Previous Step"), + ("ReadPreviousStep", "Read Previous Step"), + ("SourceVoxelSpacing", "Source Voxel Spacing"), + ("HTTPServer", "HTTP Server"), + ("Channel", "Channel"), + ], +) +def test_display_text_owns_parameter_and_class_name_spacing(name: str, expected: str) -> None: + text = FieldDisplayText.from_field_name(name) + assert text.display_name == expected + assert text.field_label == f"{expected}:" + assert text.checkbox_label == f"Enable {expected}" + assert text.group_title == expected + + +@pytest.mark.parametrize("metaclass", [type, ABCMeta]) +def test_class_valued_choice_uses_shared_preview_formatter(metaclass: type) -> None: + choice = metaclass("ReadPreviousStep", (), {"name": "previous"}) + assert format_preview_value(choice) == "Read Previous Step" + request = PreviewFieldFormatRequest("processing.input_source", choice, object) + assert ManagerPreviewFieldFormatter().format_field(request) == "input_source:Read Previous Step" + assert request.value is choice + assert choice.name == "previous" + + +def test_lazy_class_caption_uses_the_original_declaration() -> None: + @dataclass + class SourceVoxelSpacing: + value: int = 1 + + lazy = LazyDataclassFactory.make_lazy_simple(SourceVoxelSpacing, "LazyCaptionSpacing") + assert format_preview_value(lazy) == "Source Voxel Spacing" + + +def test_callable_preview_keeps_function_name() -> None: + def read_previous_step() -> None: + pass + + assert format_preview_value(read_previous_step) == "read_previous_step"