From 119a2082dfdb42b9dbca5fca1965d17b31ebaa10 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 09:49:33 -0400 Subject: [PATCH 1/8] Present admitted state fields without a second form tree --- .../forms/parameter_form_service.py | 79 +++++++++---------- src/formbridge/forms/parameter_info_types.py | 7 +- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/src/formbridge/forms/parameter_form_service.py b/src/formbridge/forms/parameter_form_service.py index 37468b9..4f3da50 100644 --- a/src/formbridge/forms/parameter_form_service.py +++ b/src/formbridge/forms/parameter_form_service.py @@ -13,6 +13,7 @@ Callable as CallableABC, ) from collections.abc import ( + Iterator, Mapping as MappingABC, ) from collections.abc import ( @@ -24,12 +25,14 @@ from objectstate import ( DataclassFieldAccess, LazyDefaultPlaceholderService, + ObjectState, UIParameterVisibilityRequest, has_lazy_resolution, should_hide_ui_parameter, ) from python_introspect import ( AnnotationChoices, + UnifiedParameterAnalyzer, coerce_enum_member, declared_annotation_choices, is_union_type, @@ -129,6 +132,30 @@ def __init__(self): """ self._type_utils = ParameterTypeUtils() + def parameters_for_state( + self, state: ObjectState, parent_path: str = "" + ) -> Iterator[ParameterInfo]: + """Present admitted direct children without extracting a second form tree. + + ObjectState owns the declarations, order and current values. This + service owns UI visibility and field-kind selection, for every frontend. + The yielded information is consumed immediately, not retained as state. + """ + parent_type = state.type_for_path(parent_path) + for path in state.direct_parameter_paths(parent_path): + declaration = state.parameter_structure.declarations[path.value] + name = declaration.name + if self._should_hide_from_ui(parent_type, name, declaration.param_type): + continue + yield create_parameter_info( + name=name, + param_type=declaration.param_type, + current_value=state.get_resolved_value(path.value), + default_value=declaration.default_value, + description=declaration.description, + is_required=declaration.is_required, + ) + def analyze_parameters(self, input: ParameterAnalysisInput) -> FormStructure: """ Analyze parameters and create form structure. @@ -253,7 +280,8 @@ def _should_hide_from_ui( ) def _unwrap_optional_dataclass_type(self, param_type: type) -> type: - """Return the dataclass type behind Optional[T], or the type itself.""" + """Return the structural type, retaining metadata on the declaration.""" + param_type = resolve_annotated(param_type) if self._type_utils.is_optional_dataclass(param_type): return self._type_utils.get_optional_inner_type(param_type) return param_type @@ -707,27 +735,15 @@ def extract_nested_parameters( parameters = ParameterDefaultsByName() parameter_types = ParameterTypesByName() - for field in dataclasses.fields(obj_type): - # Always extract actual field values when dataclass instance exists - # This preserves concrete user-entered values in nested lazy dataclass forms - if dataclass_instance is not None: - current_value = self._get_field_value(dataclass_instance, field) - else: - current_value = None # Only use None when no instance exists - - parameters[field.name] = current_value - parameter_types[field.name] = field.type + for name, declaration in UnifiedParameterAnalyzer.analyze(obj_type).items(): + parameters[name] = ( + DataclassFieldAccess.raw_value(dataclass_instance, name) + if dataclass_instance is not None else None + ) + parameter_types[name] = declaration.param_type return parameters, parameter_types - def _get_field_value( - self, dataclass_instance: FormObject | None, field: dataclasses.Field - ) -> ParameterValue: - """Extract a single field value from a dataclass instance.""" - if dataclass_instance is None: - return field.default - return DataclassFieldAccess.raw_value(dataclass_instance, field.name) - def _create_parameter_info( self, param_name: str, @@ -756,9 +772,6 @@ def _create_parameter_info( description=description, ) - # Class-level cache for nested dataclass parameter info (descriptions only) - _nested_param_info_cache = {} - def _analyze_nested_dataclass( self, param_name: str, @@ -768,31 +781,15 @@ def _analyze_nested_dataclass( parent_obj_type: type = None, ) -> FormStructure: """Analyze a nested dataclass parameter.""" - # Get the actual dataclass type - if self._type_utils.is_optional_dataclass(param_type): - obj_type = self._type_utils.get_optional_inner_type(param_type) - else: - obj_type = param_type + obj_type = self._unwrap_optional_dataclass_type(param_type) # Extract nested parameters using parent context nested_params, nested_types = self.extract_nested_parameters( current_value, obj_type, parent_obj_type ) - # OPTIMIZATION: Cache parameter info (descriptions) by dataclass type - # We only need descriptions, not instance values, so analyze the type once and reuse - cache_key = obj_type - if cache_key in self._nested_param_info_cache: - nested_param_info = self._nested_param_info_cache[cache_key] - else: - # Recursively analyze nested structure with proper descriptions for nested fields - # Use existing infrastructure to extract field descriptions for the nested dataclass - from python_introspect import UnifiedParameterAnalyzer - - # OPTIMIZATION: Always analyze the TYPE, not the instance - # This allows caching and avoids extracting field values we don't need - nested_param_info = UnifiedParameterAnalyzer.analyze(obj_type) - self._nested_param_info_cache[cache_key] = nested_param_info + # SignatureAnalyzer owns declaration caching and lazy help materialization. + nested_param_info = UnifiedParameterAnalyzer.analyze(obj_type) # Create type-safe input for recursive analysis # CRITICAL FIX: Use dotted paths for descriptions to match state._parameter_descriptions diff --git a/src/formbridge/forms/parameter_info_types.py b/src/formbridge/forms/parameter_info_types.py index 6ac6081..cf3ba09 100644 --- a/src/formbridge/forms/parameter_info_types.py +++ b/src/formbridge/forms/parameter_info_types.py @@ -45,7 +45,7 @@ def matches(param_type): ... from dataclasses import dataclass, is_dataclass from objectstate.lazy_factory import get_base_type_for_lazy -from python_introspect import optional_member_type +from python_introspect import optional_member_type, resolve_annotated from formbridge.forms.parameter_value_contracts import ParameterValue @@ -340,9 +340,10 @@ def create_parameter_info( >>> type(info3).__name__ 'GenericInfo' """ - # Iterate through registered types and find first match + # Match structural identity once; the admitted declaration keeps its metadata. + structural_type = resolve_annotated(param_type) for info_class in ParameterInfoMeta.get_registry(): - if info_class.matches(param_type): + if info_class.matches(structural_type): return info_class( name=name, type=param_type, From 32d04b147ac8fd5716319e6ebb7337b625573e88 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 13:03:35 -0400 Subject: [PATCH 2/8] Carry admitted paths with parameter presentation kinds --- src/formbridge/forms/parameter_form_service.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/formbridge/forms/parameter_form_service.py b/src/formbridge/forms/parameter_form_service.py index 4f3da50..7e62acf 100644 --- a/src/formbridge/forms/parameter_form_service.py +++ b/src/formbridge/forms/parameter_form_service.py @@ -24,6 +24,7 @@ from objectstate import ( DataclassFieldAccess, + DottedFieldPath, LazyDefaultPlaceholderService, ObjectState, UIParameterVisibilityRequest, @@ -134,8 +135,8 @@ def __init__(self): def parameters_for_state( self, state: ObjectState, parent_path: str = "" - ) -> Iterator[ParameterInfo]: - """Present admitted direct children without extracting a second form tree. + ) -> Iterator[tuple[DottedFieldPath, ParameterInfo]]: + """Present admitted paths and their kinds without extracting a second form tree. ObjectState owns the declarations, order and current values. This service owns UI visibility and field-kind selection, for every frontend. @@ -147,7 +148,7 @@ def parameters_for_state( name = declaration.name if self._should_hide_from_ui(parent_type, name, declaration.param_type): continue - yield create_parameter_info( + yield path, create_parameter_info( name=name, param_type=declaration.param_type, current_value=state.get_resolved_value(path.value), From 360bc06a1154bf6edc200fe4cf8501e3845062e3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 13:50:32 -0400 Subject: [PATCH 3/8] Derive preview containers through DottedFieldPath parenting --- .../strategies/preview_formatting.py | 8 +---- tests/test_preview_formatter_registry.py | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/formbridge/strategies/preview_formatting.py b/src/formbridge/strategies/preview_formatting.py index 07da0f4..1d9be2e 100644 --- a/src/formbridge/strategies/preview_formatting.py +++ b/src/formbridge/strategies/preview_formatting.py @@ -224,7 +224,7 @@ def collect_and_render( if not label: continue - container_path = self._container_path(field_path) + container_path = DottedFieldPath(field_path).parent builder.add_field( field_path, label, @@ -244,9 +244,3 @@ def _field_owner_for_path( f"Preview field {field_path!r} requires a nominal type or callable declaration" ) return declaration - - @staticmethod - def _container_path(field_path: str) -> DottedFieldPath: - if "." not in field_path: - return DottedFieldPath("") - return DottedFieldPath(field_path.rsplit(".", 1)[0]) diff --git a/tests/test_preview_formatter_registry.py b/tests/test_preview_formatter_registry.py index 5d780b5..8949bbe 100644 --- a/tests/test_preview_formatter_registry.py +++ b/tests/test_preview_formatter_registry.py @@ -1,6 +1,6 @@ """Preview formatter registration behavior.""" -from dataclasses import dataclass +from dataclasses import dataclass, field import pytest from objectstate.lazy_factory import LazyDataclassFactory @@ -92,3 +92,37 @@ class SpecializedHeadedConfig(HeadedConfig): assert heading(HeadedConfig) == "cfg" assert heading(SpecializedHeadedConfig) == "cfg" assert heading(lazy_type) == "cfg" + + +def test_preview_groups_root_and_nested_fields_by_their_dotted_parent(monkeypatch) -> None: + from objectstate import ObjectState, config + + from formbridge.strategies.preview_formatting import ( + FormattingConfig, + ObjectStatePreviewFormattingService, + ) + + @dataclass + class PreviewChild: + value: int = 2 + + @dataclass + class PreviewRoot: + value: int = 1 + child: PreviewChild = field(default_factory=PreviewChild) + + monkeypatch.setattr(config, "_base_config_type", PreviewRoot) + state = ObjectState(PreviewRoot()) + service = ObjectStatePreviewFormattingService( + FormattingConfig(container_abbr_func=lambda _owner: "child") + ) + + segments = service.collect_and_render( + state, ("value", "child.value"), {}, lambda request: str(request.value) + ) + + assert segments == [ + ("root", None, ""), ("{", None, ""), ("1", "value", ""), ("}", None, ""), + ("child", "child", " | "), ("{", None, ""), + ("2", "child.value", ""), ("}", None, ""), + ] From 3478efcd549fc59867a8b4d195a18f1662ab2eb1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 14:02:41 -0400 Subject: [PATCH 4/8] Use owned leaf names in bridge conversion and preview requests --- src/formbridge/bridge/object_state.py | 4 ++-- src/formbridge/utils/preview_formatters.py | 4 ++-- tests/test_preview_formatter_registry.py | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/formbridge/bridge/object_state.py b/src/formbridge/bridge/object_state.py index b68062d..51f819d 100644 --- a/src/formbridge/bridge/object_state.py +++ b/src/formbridge/bridge/object_state.py @@ -9,7 +9,7 @@ from enum import Enum from typing import ClassVar -from objectstate import ObjectState, ObjectStateRegistry +from objectstate import DottedFieldPath, ObjectState, ObjectStateRegistry from objectstate.inspection import FieldInspection, type_name, type_qualname from python_introspect import ( JsonValue, @@ -541,7 +541,7 @@ def _converted_value( request: UiObjectStateFieldMutationRequest, ) -> object: help_target = state.type_for_path(request.field_path) - parameter_name = request.field_path.rsplit(".", 1)[-1] + parameter_name = DottedFieldPath(request.field_path).field_name parameter_info = UnifiedParameterAnalyzer.analyze(help_target).get(parameter_name) if parameter_info is None: return request.value diff --git a/src/formbridge/utils/preview_formatters.py b/src/formbridge/utils/preview_formatters.py index b0a49b4..dddb5c1 100644 --- a/src/formbridge/utils/preview_formatters.py +++ b/src/formbridge/utils/preview_formatters.py @@ -7,7 +7,7 @@ from types import FunctionType, MethodType from typing import cast -from objectstate import ParameterOwner +from objectstate import DottedFieldPath, ParameterOwner @dataclass(frozen=True, slots=True) @@ -146,4 +146,4 @@ class PreviewFieldFormatRequest: @property def field_name(self) -> str: """Return the leaf field name declared by ``field_owner``.""" - return self.field_path.rsplit(".", 1)[-1] + return DottedFieldPath(self.field_path).field_name diff --git a/tests/test_preview_formatter_registry.py b/tests/test_preview_formatter_registry.py index 8949bbe..ca88a43 100644 --- a/tests/test_preview_formatter_registry.py +++ b/tests/test_preview_formatter_registry.py @@ -94,9 +94,13 @@ class SpecializedHeadedConfig(HeadedConfig): assert heading(lazy_type) == "cfg" -def test_preview_groups_root_and_nested_fields_by_their_dotted_parent(monkeypatch) -> None: +def test_field_consumers_group_name_and_convert_root_and_nested_addresses(monkeypatch) -> None: from objectstate import ObjectState, config + from formbridge.bridge.dto import UiObjectStateFieldMutationRequest + from formbridge.bridge.object_state import ObjectStateFieldMutationService + from formbridge.utils.preview_formatters import PreviewFieldFormatRequest + from formbridge.strategies.preview_formatting import ( FormattingConfig, ObjectStatePreviewFormattingService, @@ -126,3 +130,12 @@ class PreviewRoot: ("child", "child", " | "), ("{", None, ""), ("2", "child.value", ""), ("}", None, ""), ] + for address in ("value", "child.value"): + preview = PreviewFieldFormatRequest( + field_path=address, value=7, field_owner=state.type_for_path(address) + ) + assert preview.field_name == "value" + mutation = UiObjectStateFieldMutationRequest.from_fields( + object_state_scope_id="scope", field_path=address, value="7" + ) + assert ObjectStateFieldMutationService._converted_value(state, mutation) == 7 From e98e805b47e99052296fe3e1fd5b510fac70e622 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 14:21:19 -0400 Subject: [PATCH 5/8] Convert bridge fields from admitted ParameterStructure declarations --- src/formbridge/bridge/object_state.py | 14 ++++---------- tests/test_preview_formatter_registry.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/formbridge/bridge/object_state.py b/src/formbridge/bridge/object_state.py index 51f819d..08bc644 100644 --- a/src/formbridge/bridge/object_state.py +++ b/src/formbridge/bridge/object_state.py @@ -9,11 +9,10 @@ from enum import Enum from typing import ClassVar -from objectstate import DottedFieldPath, ObjectState, ObjectStateRegistry +from objectstate import ObjectState, ObjectStateRegistry from objectstate.inspection import FieldInspection, type_name, type_qualname from python_introspect import ( JsonValue, - UnifiedParameterAnalyzer, project_dataclass, to_jsonable, ) @@ -540,16 +539,11 @@ def _converted_value( state: ObjectState, request: UiObjectStateFieldMutationRequest, ) -> object: - help_target = state.type_for_path(request.field_path) - parameter_name = DottedFieldPath(request.field_path).field_name - parameter_info = UnifiedParameterAnalyzer.analyze(help_target).get(parameter_name) - if parameter_info is None: - return request.value + declaration = state.parameter_structure.declarations[request.field_path] return ParameterFormService().convert_value_to_type( request.value, - parameter_info.param_type, - parameter_name, - help_target, + declaration.param_type, + declaration.name, ) @staticmethod diff --git a/tests/test_preview_formatter_registry.py b/tests/test_preview_formatter_registry.py index ca88a43..129346c 100644 --- a/tests/test_preview_formatter_registry.py +++ b/tests/test_preview_formatter_registry.py @@ -96,6 +96,7 @@ class SpecializedHeadedConfig(HeadedConfig): def test_field_consumers_group_name_and_convert_root_and_nested_addresses(monkeypatch) -> None: from objectstate import ObjectState, config + from python_introspect import UnifiedParameterAnalyzer from formbridge.bridge.dto import UiObjectStateFieldMutationRequest from formbridge.bridge.object_state import ObjectStateFieldMutationService @@ -130,6 +131,10 @@ class PreviewRoot: ("child", "child", " | "), ("{", None, ""), ("2", "child.value", ""), ("}", None, ""), ] + def reject_reanalysis(*_args, **_kwargs): + raise AssertionError("Conversion must use ObjectState's admitted declarations") + + monkeypatch.setattr(UnifiedParameterAnalyzer, "analyze", reject_reanalysis) for address in ("value", "child.value"): preview = PreviewFieldFormatRequest( field_path=address, value=7, field_owner=state.type_for_path(address) @@ -139,3 +144,13 @@ class PreviewRoot: object_state_scope_id="scope", field_path=address, value="7" ) assert ObjectStateFieldMutationService._converted_value(state, mutation) == 7 + + group_mutation = UiObjectStateFieldMutationRequest.from_fields( + object_state_scope_id="scope", field_path="child", value={"value": 7} + ) + assert ObjectStateFieldMutationService._converted_value(state, group_mutation) == PreviewChild(7) + unknown = UiObjectStateFieldMutationRequest.from_fields( + object_state_scope_id="scope", field_path="unknown", value="untyped" + ) + with pytest.raises(KeyError, match="unknown"): + ObjectStateFieldMutationService._converted_value(state, unknown) From 1a45b8e0af860f824e5ce7ce47491ecd6555fb43 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 14:37:53 -0400 Subject: [PATCH 6/8] Delete unused conversion owner argument and stale documentation --- src/formbridge/forms/parameter_form_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/formbridge/forms/parameter_form_service.py b/src/formbridge/forms/parameter_form_service.py index 7e62acf..d99c61e 100644 --- a/src/formbridge/forms/parameter_form_service.py +++ b/src/formbridge/forms/parameter_form_service.py @@ -305,7 +305,7 @@ def _field_metadata_declares_hidden( return field_obj.metadata.get("ui_hidden", False) def convert_value_to_type( - self, value: ParameterValue, param_type: type, param_name: str, obj_type: type = None + self, value: ParameterValue, param_type: type, param_name: str ) -> ParameterValue | None: """ Convert a value to the appropriate type for a parameter. @@ -317,7 +317,6 @@ def convert_value_to_type( value: The value to convert param_type: The target parameter type param_name: The parameter name (for debugging) - obj_type: The dataclass type (for sibling inheritance checks) Returns: The converted value From dc5d21e482d086c02b07dbfa5916f409487894d4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 15 Oct 2026 19:38:15 -0400 Subject: [PATCH 7/8] WIP: checkpoint field-kind migration before installation Unfinished source, not ready for integration or installation. Factory inheritance and frontend consumer migration are checkpointed. Nullable group editing and canonical topology transitions remain incomplete; Qt helper bodies still need to move into their behavior owners. Syntax and whitespace checked only; no behavioral tests, application or provider runs. Original refactor/native-formbridge branch is unchanged. --- .../forms/parameter_form_service.py | 57 +-- src/formbridge/forms/parameter_info_types.py | 325 ++++-------------- .../services/enum_dispatch_service.py | 171 --------- .../services/parameter_service_abc.py | 206 ----------- tests/test_parameter_form_service.py | 4 +- 5 files changed, 89 insertions(+), 674 deletions(-) delete mode 100644 src/formbridge/services/enum_dispatch_service.py delete mode 100644 src/formbridge/services/parameter_service_abc.py diff --git a/src/formbridge/forms/parameter_form_service.py b/src/formbridge/forms/parameter_form_service.py index d99c61e..24e5934 100644 --- a/src/formbridge/forms/parameter_form_service.py +++ b/src/formbridge/forms/parameter_form_service.py @@ -41,7 +41,7 @@ ) from formbridge.forms.parameter_form_constants import CONSTANTS -from formbridge.forms.parameter_info_types import ParameterInfo, create_parameter_info +from formbridge.forms.parameter_info_types import OptionalDataclassInfo, ParameterInfo from formbridge.forms.parameter_type_utils import ParameterTypeUtils from formbridge.forms.parameter_value_contracts import ( FormObject, @@ -96,7 +96,9 @@ class FormStructure: field_id: str parameters: list[ParameterInfo] nested_forms: dict[str, "FormStructure"] - has_optional_dataclasses: bool = False + @property + def has_optional_dataclasses(self) -> bool: + return any(isinstance(info, OptionalDataclassInfo) for info in self.parameters) def get_parameter_info(self, param_name: str) -> ParameterInfo | None: """ @@ -148,7 +150,7 @@ def parameters_for_state( name = declaration.name if self._should_hide_from_ui(parent_type, name, declaration.param_type): continue - yield path, create_parameter_info( + yield path, ParameterInfo.create( name=name, param_type=declaration.param_type, current_value=state.get_resolved_value(path.value), @@ -186,7 +188,6 @@ def analyze_parameters(self, input: ParameterAnalysisInput) -> FormStructure: param_infos = [] nested_forms = {} - has_optional_dataclasses = False for param_name, parameter_type in input.param_type.items(): current_value = input.default_value.get(param_name) @@ -210,45 +211,23 @@ def analyze_parameters(self, input: ParameterAnalysisInput) -> FormStructure: ) param_infos.append(param_info) - # Check for nested dataclasses using isinstance (type-safe!) - from formbridge.forms.parameter_info_types import ( - DirectDataclassInfo, - OptionalDataclassInfo, - ) - - if isinstance(param_info, (OptionalDataclassInfo, DirectDataclassInfo)): - # Get actual field path from FieldPathDetector (no artificial "nested_" prefix) - # Unwrap Optional types to get the actual dataclass type for field path detection - unwrapped_param_type = self._unwrap_optional_dataclass_type(parameter_type) - - # For function parameters (no parent dataclass), use parameter name directly - if input.parent_obj_type is None: - nested_field_id = param_name - else: - nested_field_id = self.resolve_declared_field_path( - input.parent_obj_type, - param_name, - unwrapped_param_type, - ) - - nested_structure = self._analyze_nested_dataclass( - param_name, - parameter_type, - current_value, - nested_field_id, - input.parent_obj_type, - ) + nested_structure = param_info.nested_form(self, input) + if nested_structure is not None: nested_forms[param_name] = nested_structure - # Check for optional dataclasses using isinstance (type-safe!) - if isinstance(param_info, OptionalDataclassInfo): - has_optional_dataclasses = True - return FormStructure( field_id=input.field_id, parameters=param_infos, nested_forms=nested_forms, - has_optional_dataclasses=has_optional_dataclasses, + ) + + def analyze_nested_parameter(self, info: ParameterInfo, analysis: ParameterAnalysisInput) -> FormStructure: + """Build the nested presentation requested by the admitted kind.""" + unwrapped_type = self._unwrap_optional_dataclass_type(info.type) + nested_field_id = (info.name if analysis.parent_obj_type is None else + self.resolve_declared_field_path(analysis.parent_obj_type, info.name, unwrapped_type)) + return self._analyze_nested_dataclass( + info.name, info.type, info.current_value, nested_field_id, analysis.parent_obj_type, ) def _should_hide_from_ui( @@ -756,7 +735,7 @@ def _create_parameter_info( Create parameter information object using discriminated union factory. Uses type introspection to automatically select the correct ParameterInfo - subclass (OptionalDataclassInfo, DirectDataclassInfo, or GenericInfo). + most specific admitted ParameterInfo subclass. """ description = None resolved_info = parameter_info() if callable(parameter_info) else parameter_info @@ -765,7 +744,7 @@ def _create_parameter_info( # Use factory to create correct ParameterInfo subclass # Factory uses type introspection to determine which type to create - return create_parameter_info( + return ParameterInfo.create( name=param_name, param_type=param_type, current_value=current_value, diff --git a/src/formbridge/forms/parameter_info_types.py b/src/formbridge/forms/parameter_info_types.py index cf3ba09..ca54762 100644 --- a/src/formbridge/forms/parameter_info_types.py +++ b/src/formbridge/forms/parameter_info_types.py @@ -1,46 +1,10 @@ -""" -Discriminated union types for parameter information. - -This module implements React-style discriminated unions for type-safe parameter handling. -Instead of using boolean flags (is_optional, is_nested), we use polymorphic types that -are automatically selected based on type annotations. - -Key features: -1. Metaclass auto-registration - all ParameterInfo subclasses auto-register -2. Type-driven factory - create_parameter_info() uses type introspection -3. Zero boilerplate - just define new dataclass with matches() predicate -4. Type-safe dispatch - services use class name for automatic dispatch - -Pattern (React-style): - Instead of: - @dataclass - class ParameterInfo: - is_optional: bool - is_nested: bool - - if info.is_optional and info.is_nested: - # handle optional dataclass - - Use: - @dataclass - class OptionalDataclassInfo(metaclass=ParameterInfoMeta): - @staticmethod - def matches(param_type): ... - - if isinstance(info, OptionalDataclassInfo): - # Type checker knows info is OptionalDataclassInfo! - -Architecture: - - ParameterInfoMeta: Metaclass that auto-registers all subclasses - - ParameterInfoBase: Base class for all parameter info types - - OptionalDataclassInfo: Optional[Dataclass] parameters (checkbox-controlled) - - DirectDataclassInfo: Direct Dataclass parameters (always exists) - - GenericInfo: Generic parameters (simple widgets) - - create_parameter_info(): Factory that auto-selects correct type +"""Parameter declarations select their most specific kind through inheritance. + +The root is an ordinary value. Nullable and dataclass declarations compose; +inline factories specialize dataclasses without changing their value identity. """ -import logging -from abc import ABC, ABCMeta +from abc import ABCMeta from collections.abc import Callable from dataclasses import dataclass, is_dataclass @@ -49,23 +13,17 @@ def matches(param_type): ... from formbridge.forms.parameter_value_contracts import ParameterValue -logger = logging.getLogger(__name__) - InlineDataclassWidgetFactory = Callable class _InlineDataclassWidgetCatalog: - """Private registry authority for dataclasses with structural inline editors.""" + """Factories for structural inline editors, not a second kind registry.""" _factories: dict[type, InlineDataclassWidgetFactory] = {} @classmethod - def register( - cls, - dataclass_type: type, - widget_factory: InlineDataclassWidgetFactory, - ) -> None: + def register(cls, dataclass_type: type, widget_factory: InlineDataclassWidgetFactory) -> None: if not is_dataclass(dataclass_type): raise TypeError( "Inline dataclass widgets can only be registered for dataclass types; " @@ -93,14 +51,9 @@ def has_factory_for(cls, dataclass_type: type) -> bool: @classmethod def _candidate_types(cls, dataclass_type: type) -> tuple[type, ...]: - """Return exact, lazy-base, and inherited dataclass types for lookup.""" - base_type = get_base_type_for_lazy(dataclass_type) - root_types = ( - (dataclass_type,) - if base_type is None or base_type is dataclass_type - else (dataclass_type, base_type) - ) + root_types = ((dataclass_type,) if base_type is None or base_type is dataclass_type + else (dataclass_type, base_type)) candidates: list[type] = [] seen: set[type] = set() for root_type in root_types: @@ -112,249 +65,109 @@ def _candidate_types(cls, dataclass_type: type) -> tuple[type, ...]: return tuple(candidates) -def register_inline_dataclass_widget( - dataclass_type: type, - widget_factory: InlineDataclassWidgetFactory, -) -> None: - """Register a full-row editor for a dataclass type before nested fallback.""" +def register_inline_dataclass_widget(dataclass_type: type, widget_factory: InlineDataclassWidgetFactory) -> None: + """Register the structural editor capability for a dataclass.""" _InlineDataclassWidgetCatalog.register(dataclass_type, widget_factory) -@dataclass -class ParameterInfoBase(ABC): - """ABC for parameter information objects - enforces explicit interface.""" - - name: str - type: type - current_value: ParameterValue | None - description: str | None = None - - # Widget creation type - subclasses override. Imported lazily to avoid circular imports. - widget_creation_type: str = "REGULAR" # Default, overridden by subclasses - - class ParameterInfoMeta(ABCMeta): - """ - Metaclass for auto-registration of ParameterInfo types. - - All classes with a matches() method are automatically registered - in the global registry for use by the factory function. - - This eliminates manual registration and enables zero-boilerplate - addition of new parameter types. - """ + """Discover predicates from their declarations, never their import order.""" _registry: list[type] = [] def __new__(mcs, name, bases, namespace): cls = super().__new__(mcs, name, bases, namespace) - - # Auto-register if it has a matches() predicate - if "matches" in namespace and callable(namespace["matches"]): + if "matches" in namespace and callable(cls.matches): mcs._registry.append(cls) - logger.debug(f"Auto-registered ParameterInfo type: {name}") - return cls @classmethod - def get_registry(cls) -> list[type]: - """Get all registered ParameterInfo types.""" - return cls._registry.copy() + def select(mcs, root: type, annotation: object) -> type: + matches = [kind for kind in mcs._registry + if issubclass(kind, root) and kind.matches(annotation)] + if not matches: + return root + specific = [kind for kind in matches + if all(issubclass(kind, other) for other in matches)] + if len(specific) != 1: + names = ", ".join(sorted(kind.__qualname__ for kind in matches)) + raise TypeError(f"Ambiguous parameter declarations for {annotation!r}: {names}") + return specific[0] @dataclass -class OptionalDataclassInfo(ParameterInfoBase, metaclass=ParameterInfoMeta): - """ - Parameter info for Optional[Dataclass] types. - - These parameters: - - Have a checkbox to enable/disable - - Have a nested form that appears when enabled - - Can be None when checkbox is unchecked - - Support lazy inheritance from parent configs - - Examples: - def process(config: Optional[ProcessingConfig]): ... - def analyze(settings: Optional[AnalysisSettings]): ... - """ +class ParameterInfo(metaclass=ParameterInfoMeta): + """An admitted field; ordinary values are the family's concrete default.""" + name: str + type: type + current_value: ParameterValue | None + description: str | None = None default_value: ParameterValue | None = None is_required: bool = True - widget_creation_type: str = "OPTIONAL_NESTED" - - @staticmethod - def matches(param_type: type) -> bool: - """ - Predicate: Does this type annotation match Optional[Dataclass]? - Returns True if: - - Type is Union[T, None] (i.e., Optional[T]) - - T is a dataclass - """ - inner_type = optional_member_type(param_type) - return inner_type is not None and is_dataclass(inner_type) + @classmethod + def create(cls, name: str, param_type: type, current_value: ParameterValue | None, + default_value: ParameterValue | None = None, description: str | None = None, + is_required: bool = True) -> "ParameterInfo": + kind = ParameterInfoMeta.select(cls, resolve_annotated(param_type)) + return kind(name=name, type=param_type, current_value=current_value, + default_value=default_value, description=description, is_required=is_required) + + def nested_form(self, service, analysis): + """Ordinary values do not create a nested form.""" + return None @dataclass -class InlineDataclassWidgetInfo(ParameterInfoBase, metaclass=ParameterInfoMeta): - """ - Parameter info for dataclasses with a registered structural inline editor. - - These parameters: - - Are still regular dataclass-backed values - - Use a registered widget instead of the default nested field form - - Fall back to DirectDataclassInfo when no inline widget is registered - """ - - default_value: ParameterValue | None = None - is_required: bool = True - widget_creation_type: str = "INLINE_DATACLASS" +class DirectDataclassInfo(ParameterInfo): + """A dataclass value with the ordinary nested-form presentation.""" @staticmethod def matches(param_type: type) -> bool: - return is_dataclass(param_type) and _InlineDataclassWidgetCatalog.has_factory_for( - param_type - ) + return is_dataclass(param_type) - def widget_factory(self) -> InlineDataclassWidgetFactory: - factory = _InlineDataclassWidgetCatalog.factory_for(self.type) - if factory is None: - raise ValueError( - "No inline dataclass widget registered for " - f"{self.type!r}; DirectDataclassInfo should have handled it." - ) - return factory + def nested_form(self, service, analysis): + return service.analyze_nested_parameter(self, analysis) @dataclass -class DirectDataclassInfo(ParameterInfoBase, metaclass=ParameterInfoMeta): - """ - Parameter info for direct Dataclass types (non-optional). - - These parameters: - - Always exist (never None) - - Have a nested form that's always visible - - Preserve object identity during reset - - Don't have a checkbox - - Examples: - def process(config: ProcessingConfig): ... - def analyze(settings: AnalysisSettings): ... - """ - - default_value: ParameterValue | None = None - is_required: bool = True - widget_creation_type: str = "NESTED" +class OptionalInfo(ParameterInfo): + """A nullable value; nullability is independent of its member's shape.""" @staticmethod def matches(param_type: type) -> bool: - """ - Predicate: Does this type annotation match a direct Dataclass? - - Returns True if: - - Type is a dataclass - - Type is NOT Optional - """ - return is_dataclass(param_type) + return optional_member_type(param_type) is not None @dataclass -class GenericInfo(ParameterInfoBase, metaclass=ParameterInfoMeta): - """ - Parameter info for generic types (int, str, Path, etc.). - - These parameters: - - Use simple widgets (QLineEdit, QSpinBox, etc.) - - Don't have nested forms - - Support lazy inheritance via placeholders - - Are the most common parameter type - - Examples: - def process(threshold: int): ... - def analyze(input_path: Path): ... - def filter(sigma: float): ... - """ - - default_value: ParameterValue | None = None - is_required: bool = True +class OptionalDataclassInfo(OptionalInfo, DirectDataclassInfo): + """A nullable dataclass composes both declarations through C3 MRO.""" @staticmethod def matches(param_type: type) -> bool: - """ - Predicate: Fallback - matches everything. - - This should be registered LAST in the registry so it acts - as a catch-all for any types not matched by other predicates. - """ - return True - + inner_type = optional_member_type(param_type) + return inner_type is not None and is_dataclass(inner_type) -# Union type for type hints (React-style) -ParameterInfo = ( - OptionalDataclassInfo | InlineDataclassWidgetInfo | DirectDataclassInfo | GenericInfo -) +@dataclass +class InlineDataclassWidgetInfo(DirectDataclassInfo): + """A dataclass with a structural widget factory. -def create_parameter_info( - name: str, - param_type: type, - current_value: ParameterValue | None, - default_value: ParameterValue | None = None, - description: str | None = None, - is_required: bool = True, -) -> ParameterInfo: + A frontend without that factory capability still has the dataclass + presentation through the parent; it never calls another toolkit's factory. """ - Factory function that auto-selects the correct ParameterInfo subclass. - - Uses type introspection to determine which ParameterInfo type to create. - This eliminates manual if-elif-else chains and enables type-safe dispatch. - - Args: - name: Parameter name - param_type: Parameter type annotation - current_value: Current parameter value - default_value: Default parameter value - description: Parameter description - is_required: Whether parameter is required - - Returns: - Correct ParameterInfo subclass instance - - Raises: - ValueError: If no matching ParameterInfo type found - - Examples: - >>> from typing import Optional - >>> @dataclass - ... class Config: pass - >>> info1 = create_parameter_info('config', Optional[Config], None) - >>> type(info1).__name__ - 'OptionalDataclassInfo' - - >>> info2 = create_parameter_info('config', Config, Config()) - >>> type(info2).__name__ - 'DirectDataclassInfo' + @staticmethod + def matches(param_type: type) -> bool: + return is_dataclass(param_type) and _InlineDataclassWidgetCatalog.has_factory_for(param_type) - >>> info3 = create_parameter_info('value', int, 42) - >>> type(info3).__name__ - 'GenericInfo' - """ - # Match structural identity once; the admitted declaration keeps its metadata. - structural_type = resolve_annotated(param_type) - for info_class in ParameterInfoMeta.get_registry(): - if info_class.matches(structural_type): - return info_class( - name=name, - type=param_type, - current_value=current_value, - default_value=default_value, - description=description, - is_required=is_required, - ) + def nested_form(self, service, analysis): + # This frontend uses the structural value widget, not a nested manager. + return None - # Should never reach here due to GenericInfo fallback - raise ValueError( - f"No matching ParameterInfo type for {param_type}. " - f"This should never happen - GenericInfo should match everything." - ) + def widget_factory(self) -> InlineDataclassWidgetFactory: + factory = _InlineDataclassWidgetCatalog.factory_for(resolve_annotated(self.type)) + if factory is None: + raise ValueError(f"No inline dataclass widget registered for {self.type!r}") + return factory diff --git a/src/formbridge/services/enum_dispatch_service.py b/src/formbridge/services/enum_dispatch_service.py deleted file mode 100644 index fae02d3..0000000 --- a/src/formbridge/services/enum_dispatch_service.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -Abstract base class for enum-driven polymorphic dispatch services. - -This ABC eliminates duplication across services that use the same pattern: -1. Define an enum for strategies/types -2. Create a dispatch table mapping enum values to handler methods -3. Determine which strategy to use based on input -4. Dispatch to the appropriate handler - -Services using this pattern: -- ParameterResetService (ResetStrategy enum) -- NestedValueCollectionService (ValueCollectionStrategy enum) -- Widget creation (WidgetCreationType enum) - -Benefits: -- Single source of truth for dispatch pattern -- Enforces consistent structure across services -- Reduces boilerplate in service implementations -- Makes adding new services trivial - -Example: - class MyStrategy(Enum): - TYPE_A = "type_a" - TYPE_B = "type_b" - - class MyService(EnumDispatchService[MyStrategy]): - def __init__(self): - super().__init__() - self._register_handlers({ - MyStrategy.TYPE_A: self._handle_type_a, - MyStrategy.TYPE_B: self._handle_type_b, - }) - - def _determine_strategy(self, context, **kwargs) -> MyStrategy: - # Logic to determine which strategy to use - return MyStrategy.TYPE_A if some_condition else MyStrategy.TYPE_B - - def _handle_type_a(self, context, **kwargs): - # Handler implementation - pass - - def _handle_type_b(self, context, **kwargs): - # Handler implementation - pass - - def process(self, context, **kwargs): - # Public API - uses dispatch - return self.dispatch(context, **kwargs) -""" - -import logging -from abc import ABC, abstractmethod -from collections.abc import Callable -from enum import Enum -from typing import Any, Generic, TypeVar - -logger = logging.getLogger(__name__) - -# Type variable for the strategy enum -StrategyEnum = TypeVar("StrategyEnum", bound=Enum) - - -class EnumDispatchService(ABC, Generic[StrategyEnum]): - """ - Abstract base class for services using enum-driven polymorphic dispatch. - - Subclasses must: - 1. Define a strategy enum (e.g., ResetStrategy, ValueCollectionStrategy) - 2. Implement _determine_strategy() to select the appropriate strategy - 3. Register handlers in __init__() using _register_handlers() - 4. Implement handler methods for each strategy - - The dispatch() method handles the actual dispatching logic. - """ - - def __init__(self): - """Initialize the service with an empty handler registry.""" - self._handlers: dict[StrategyEnum, Callable] = {} - - def _register_handlers(self, handlers: dict[StrategyEnum, Callable]) -> None: - """ - Register strategy handlers. - - Args: - handlers: Dictionary mapping strategy enum values to handler methods - - Raises: - ValueError: If handlers dict is empty or contains invalid strategies - """ - if not handlers: - raise ValueError(f"{self.__class__.__name__}: Handler registry cannot be empty") - - self._handlers = handlers - logger.debug(f"{self.__class__.__name__}: Registered {len(handlers)} handlers") - - @abstractmethod - def _determine_strategy(self, *args, **kwargs) -> StrategyEnum: - """ - Determine which strategy to use based on input. - - This method must be implemented by subclasses to analyze the input - and return the appropriate strategy enum value. - - Returns: - Strategy enum value indicating which handler to use - """ - pass - - def dispatch(self, *args, **kwargs) -> Any: - """ - Dispatch to the appropriate handler based on determined strategy. - - This is the core dispatch logic that: - 1. Determines the strategy using _determine_strategy() - 2. Looks up the handler in the registry - 3. Calls the handler with the provided arguments - - Args: - *args: Positional arguments to pass to the handler - **kwargs: Keyword arguments to pass to the handler - - Returns: - Result from the handler method - - Raises: - KeyError: If strategy is not registered in handlers - """ - # Determine which strategy to use - strategy = self._determine_strategy(*args, **kwargs) - - # Look up handler - if strategy not in self._handlers: - raise KeyError( - f"{self.__class__.__name__}: No handler registered for strategy {strategy}. " - f"Available strategies: {list(self._handlers.keys())}" - ) - # Dispatch to handler - handler = self._handlers[strategy] - logger.debug(f"{self.__class__.__name__}: Dispatching to {strategy.value} handler") - - # Handler call convention: the first positional argument is the - # primary context (e.g., manager/context object). Additional - # positional arguments passed to dispatch are used only for - # determining the strategy (for example a pre-computed 'mode') and - # should NOT be forwarded to the handler. Forward only the - # primary context and keyword arguments to the handler to avoid - # accidental "takes X positional arguments but Y were given" - # errors when handlers are bound instance methods. - handler_args = args[:1] if args else () - return handler(*handler_args, **kwargs) - - def get_registered_strategies(self) -> list[StrategyEnum]: - """ - Get list of all registered strategies. - - Returns: - List of strategy enum values that have registered handlers - """ - return list(self._handlers.keys()) - - def has_strategy(self, strategy: StrategyEnum) -> bool: - """ - Check if a strategy has a registered handler. - - Args: - strategy: Strategy enum value to check - - Returns: - True if strategy has a registered handler, False otherwise - """ - return strategy in self._handlers diff --git a/src/formbridge/services/parameter_service_abc.py b/src/formbridge/services/parameter_service_abc.py deleted file mode 100644 index 8e43ca8..0000000 --- a/src/formbridge/services/parameter_service_abc.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Abstract base class for parameter services with auto-discovery dispatch. - -This module provides a unified pattern for all services that operate on parameters -based on their ParameterInfo type. It eliminates code duplication and enforces -consistent architecture across all parameter services. - -Key features: -1. Auto-discovery of handler methods based on naming convention -2. Type-safe dispatch using ParameterInfo discriminated unions -3. Zero boilerplate - just define handler methods -4. Consistent pattern across all services - -Pattern: - Instead of: - class MyService: - def process(self, info): - if isinstance(info, OptionalDataclassInfo): - # handle optional dataclass - elif isinstance(info, DirectDataclassInfo): - # handle direct dataclass - else: - # handle generic - - Use: - class MyService(ParameterServiceABC): - def _get_handler_prefix(self) -> str: - return '_process_' - - def _process_OptionalDataclassInfo(self, info, ...): - # Type checker knows info is OptionalDataclassInfo! - ... - - def _process_DirectDataclassInfo(self, info, ...): - # Type checker knows info is DirectDataclassInfo! - ... - - def _process_GenericInfo(self, info, ...): - # Type checker knows info is GenericInfo! - ... - -Services using this pattern: -- ParameterResetService: _reset_OptionalDataclassInfo, etc. -- NestedValueCollectionService: _collect_OptionalDataclassInfo, etc. -- Future services: just inherit and define handlers - -Architecture benefits: -- Single source of truth for dispatch logic -- No if-elif-else chains -- No manual registry maintenance -- Type-safe (type checker narrows in each handler) -- Adding new ParameterInfo type = add handler method to all services -""" - -import logging -from abc import ABC, abstractmethod -from collections.abc import Callable -from typing import Any - -from formbridge.forms.parameter_info_types import ParameterInfo - -logger = logging.getLogger(__name__) - - -class ParameterServiceABC(ABC): - """ - Abstract base for parameter services with auto-discovery dispatch. - - Subclasses must: - 1. Implement _get_handler_prefix() to return method prefix (e.g., '_reset_') - 2. Define handler methods following naming convention: {prefix}{ClassName} - - The ABC automatically discovers all handler methods and provides - type-safe dispatch via the dispatch() method. - - Examples: - class ResetService(ParameterServiceABC): - def _get_handler_prefix(self) -> str: - return '_reset_' - - def reset_parameter(self, manager, param_name: str): - info = manager.form_structure.get_parameter_info(param_name) - self.dispatch(info, manager) - - def _reset_OptionalDataclassInfo(self, info: OptionalDataclassInfo, manager): - # Handler for Optional[Dataclass] parameters - ... - - def _reset_DirectDataclassInfo(self, info: DirectDataclassInfo, manager): - # Handler for direct Dataclass parameters - ... - - def _reset_GenericInfo(self, info: GenericInfo, manager): - # Handler for generic parameters - ... - """ - - def __init__(self): - """ - Initialize service and auto-discover handler methods. - - Discovers all methods matching the pattern: {prefix}{ClassName} - where prefix is returned by _get_handler_prefix(). - """ - self._handlers: dict[str, Callable] = {} - prefix = self._get_handler_prefix() - - # Auto-discover handlers by introspecting methods - for attr_name in dir(self): - if attr_name.startswith(prefix): - # Extract class name from method name - # e.g., '_reset_OptionalDataclassInfo' -> 'OptionalDataclassInfo' - class_name = attr_name.replace(prefix, "") - handler = getattr(self, attr_name) - - # Verify it's callable - if callable(handler): - self._handlers[class_name] = handler - - # Log discovered handlers for debugging - if self._handlers: - logger.debug( - f"{self.__class__.__name__} auto-discovered handlers: {list(self._handlers.keys())}" - ) - else: - logger.warning( - f"{self.__class__.__name__} found no handlers with prefix '{prefix}'. " - f"Did you forget to define handler methods?" - ) - - @abstractmethod - def _get_handler_prefix(self) -> str: - """ - Return the method prefix for this service's handlers. - - Examples: - - ParameterResetService: '_reset_' - - NestedValueCollectionService: '_collect_' - - WidgetUpdateService: '_update_' - - Returns: - Method prefix string (must include leading underscore) - """ - pass - - def dispatch(self, info: ParameterInfo, *args, **kwargs) -> Any: - """ - Auto-dispatch to handler based on ParameterInfo class name. - - This method provides type-safe dispatch without if-elif-else chains. - The type checker can narrow the type in each handler method. - - Args: - info: ParameterInfo instance (discriminated union) - *args: Additional positional arguments passed to handler - **kwargs: Additional keyword arguments passed to handler - - Returns: - Result from handler method - - Raises: - ValueError: If no handler found for ParameterInfo type - - Examples: - >>> service = ResetService() - >>> info = OptionalDataclassInfo(...) - >>> service.dispatch(info, manager) # Calls _reset_OptionalDataclassInfo - """ - class_name = info.__class__.__name__ - handler = self._handlers.get(class_name) - - if handler is None: - raise ValueError( - f"No handler for {class_name} in {self.__class__.__name__}. " - f"Available handlers: {list(self._handlers.keys())}. " - f"Did you forget to define {self._get_handler_prefix()}{class_name}()?" - ) - - # Call handler with info as first argument, followed by additional args - return handler(info, *args, **kwargs) - - def has_handler(self, info: ParameterInfo) -> bool: - """ - Check if a handler exists for the given ParameterInfo type. - - Useful for conditional logic or validation. - - Args: - info: ParameterInfo instance to check - - Returns: - True if handler exists, False otherwise - """ - class_name = info.__class__.__name__ - return class_name in self._handlers - - def get_supported_types(self) -> list[str]: - """ - Get list of supported ParameterInfo type names. - - Useful for debugging and validation. - - Returns: - List of class names that have handlers - """ - return list(self._handlers.keys()) diff --git a/tests/test_parameter_form_service.py b/tests/test_parameter_form_service.py index b0ce1e0..2cc69a5 100644 --- a/tests/test_parameter_form_service.py +++ b/tests/test_parameter_form_service.py @@ -8,7 +8,7 @@ import pytest from formbridge.forms.parameter_form_service import ParameterFormService -from formbridge.forms.parameter_info_types import OptionalDataclassInfo, create_parameter_info +from formbridge.forms.parameter_info_types import OptionalDataclassInfo, ParameterInfo from formbridge.forms.parameter_type_utils import ParameterTypeUtils @@ -185,7 +185,7 @@ def test_pep604_optional_dataclass_uses_shared_optional_authority() -> None: assert ParameterTypeUtils.is_optional_dataclass(annotation) assert ParameterTypeUtils.get_optional_inner_type(annotation) is NestedConfig assert isinstance( - create_parameter_info("config", annotation, None), + ParameterInfo.create("config", annotation, None), OptionalDataclassInfo, ) From 5670ae670de81f3856e45dd312387dddfa652d34 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 16 Oct 2026 18:08:37 -0400 Subject: [PATCH 8/8] Release 0.2.0 against ObjectState 5 Removes public names (ParameterInfoBase, GenericInfo, create_parameter_info, EnumDispatchService, ParameterServiceABC, the converter's fourth argument), so this is a minor bump under 0.x. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 14 ++++++++++++++ pyproject.toml | 2 +- src/formbridge/__init__.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c52673..2bc933b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.2.0 + +Requires `objectstate>=5.0.0,<6` (#13). + +Parameter kinds select themselves through inheritance: `ParameterInfo.create` +picks the most specific declared kind (`DirectDataclassInfo`, `OptionalInfo`, +`OptionalDataclassInfo`, `InlineDataclassWidgetInfo`), and each kind supplies +its own `nested_form`. Bridge conversion reads the admitted declaration from +`state.parameter_structure.declarations` and takes three arguments. + +Removed: `ParameterInfoBase`, `GenericInfo` (use `ParameterInfo`), +`create_parameter_info` (use `ParameterInfo.create`), `EnumDispatchService`, +`ParameterServiceABC`, and the converter's fourth `obj_type` argument. + ## 0.1.5 Requires `python-introspect>=0.3.0,<0.4` (was `<0.3`), `objectstate>=4.0.1,<5` diff --git a/pyproject.toml b/pyproject.toml index a528d0c..dc777a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ keywords = ["forms", "reactive", "dataclass", "ui-bridge", "headless"] dependencies = [ "docutils>=0.20", "metaclass-registry>=0.4.0", - "objectstate>=4.0.1,<5", + "objectstate>=5.0.0,<6", "psutil>=5.9", "python-introspect>=0.3.0,<0.4", "pyzmq>=22.0", diff --git a/src/formbridge/__init__.py b/src/formbridge/__init__.py index 2677349..bfe383a 100644 --- a/src/formbridge/__init__.py +++ b/src/formbridge/__init__.py @@ -4,4 +4,4 @@ nothing and loads no GUI toolkit. """ -__version__ = "0.1.5" +__version__ = "0.2.0"