Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 0.9.0

Requires `objectstate>=5.0.0,<6` and `formbridge>=0.2.0,<0.3` (#33).

Field paths come from ObjectState's `DottedFieldPath` (`parent`, `lineage`,
`field_name`) instead of splitting dotted strings, and each form manager's
`_object_state_path_for_field` returns a `DottedFieldPath`. Parameter
conversion takes three arguments, matching FormBridge 0.2.

## 0.8.0

Requires ObjectState 4 (`objectstate>=4.0.0,<5`) and `formbridge>=0.1.4,<0.2`.
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ keywords = ["pyqt6", "forms", "reactive", "gui", "widgets", "dataclass"]

dependencies = [
"annotated-types>=0.7.0",
"formbridge>=0.1.4,<0.2",
"formbridge>=0.2.0,<0.3",
"magicgui>=0.7.0",
"metaclass-registry>=0.4.0",
"PyQt6>=6.4.0",
"objectstate>=4.0.0,<5",
"objectstate>=5.0.0,<6",
"psutil>=5.9",
"Pygments>=2.15",
"python-introspect>=0.2.6,<0.3",
"zmqruntime>=0.6.4,<0.7",
"python-introspect>=0.3.0,<0.4",
"zmqruntime>=0.6.5,<0.7",
]

[project.optional-dependencies]
Expand Down
2 changes: 0 additions & 2 deletions requirements-ci.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
# Source candidates needed to validate this checkout before coordinated publication.
# Empty: every first-party dependency is released (objectstate 4.0.0,
# formbridge 0.1.4, zmqruntime 0.6.4, metaclass-registry 0.4.0).
2 changes: 1 addition & 1 deletion src/pyqt_reactive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- Cross-window reactive updates
"""

__version__ = "0.8.0"
__version__ = "0.9.0"

# Public API will be populated as modules are added
__all__ = [
Expand Down
8 changes: 1 addition & 7 deletions src/pyqt_reactive/bridge/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -1794,7 +1794,7 @@ def _add_field_widget(
widget: QWidget,
action_role: str,
) -> None:
field_path = cls._field_path(form_manager, field_name)
field_path = form_manager._object_state_path_for_field(field_name).value
if field_path not in state.parameters:
return
records[_WidgetGeometryKey.from_qwidget(widget)] = _WidgetFieldSemanticRecord(
Expand All @@ -1805,12 +1805,6 @@ def _add_field_widget(
),
)

@staticmethod
def _field_path(form_manager: ParameterFormManager, field_name: str) -> str:
if form_manager.field_id:
return f"{form_manager.field_id}.{field_name}"
return field_name

def for_descriptor(
self,
descriptor: WidgetDescriptor,
Expand Down
9 changes: 5 additions & 4 deletions src/pyqt_reactive/forms/form_init_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
ParameterInfoSequence,
)
from pyqt_reactive.theming.color_scheme import ColorScheme as PyQt6ColorScheme
from objectstate import get_base_config_type, has_lazy_resolution
from objectstate import DottedFieldPath, get_base_config_type, has_lazy_resolution

if TYPE_CHECKING:
from formbridge.forms.parameter_form_service import FormStructure
Expand Down Expand Up @@ -77,8 +77,8 @@ class ExtractedParameters:
description: ParameterDescriptionByPath | ParameterDescriptionProvider = field(
default_factory=ParameterDescriptionByPath,
metadata={
'project': lambda name, info, field_id: (
f"{field_id}.{name}" if field_id else name,
'project': lambda name, info, scope_path: (
scope_path.child(name).value,
info.description,
)
}
Expand Down Expand Up @@ -509,6 +509,7 @@ def _auto_generate_builders():
"""Auto-generate all builder functions via introspection of their output types."""

def _extract_parameters(object_instance, exclude_params, initial_values, field_id=None):
scope_path = DottedFieldPath(field_id or "")
param_info_dict = UnifiedParameterAnalyzer.analyze(object_instance, exclude_params=exclude_params or [])
extracted = {}
computed = {}
Expand All @@ -521,7 +522,7 @@ def _extract_parameters(object_instance, exclude_params, initial_values, field_i
if projection is None:
raise ValueError(f"Unsupported extracted parameter field: {fld.name}")
extracted[fld.name] = dict(
projection(name, info, field_id)
projection(name, info, scope_path)
for name, info in param_info_dict.items()
)
if initial_values and fld.metadata.get('initial_values'):
Expand Down
7 changes: 2 additions & 5 deletions src/pyqt_reactive/forms/form_value_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from objectstate import DottedFieldPath
from pyqt_reactive.protocols import CurrentValueValidatable


Expand All @@ -23,11 +24,7 @@ def _validate_manager(self, manager) -> None:
widget.validate_current_value()
manager._convert_widget_value(widget.get_value(), field_name)
except ValueError as exc:
field_path = (
f"{manager.field_id}.{field_name}"
if manager.field_id
else field_name
)
field_path = DottedFieldPath(manager.field_id).child(field_name).value
raise FormValueValidationError(
f"Invalid value for {field_path!r}: {exc}"
) from exc
Expand Down
6 changes: 2 additions & 4 deletions src/pyqt_reactive/forms/inline_dataclass_chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,11 @@ def field_names_under(
self,
owner_field_paths: tuple[DottedFieldPath, ...],
) -> tuple[str, ...]:
owner_parts = self.context.owner_path.parts
return tuple(
dict.fromkeys(
path.parts[-1]
path.field_name
for path in owner_field_paths
if len(path.parts) == len(owner_parts) + 1
and path.parts[: len(owner_parts)] == owner_parts
if path.value and path.parent == self.context.owner_path
)
)

Expand Down
20 changes: 10 additions & 10 deletions src/pyqt_reactive/forms/inline_dataclass_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ class InlineDataclassChildFieldIdentity:

@property
def field_name(self) -> str:
parts = self.object_state_path.parts
if not parts:
field_name = self.object_state_path.field_name
if not field_name:
raise ValueError("Inline dataclass child field identity cannot be root.")
return parts[-1]
return field_name


@dataclass(frozen=True, slots=True)
Expand All @@ -52,11 +52,7 @@ def from_inline_widget(
param_info: "InlineDataclassWidgetInfo",
current_value,
) -> "InlineDataclassFormContext":
owner_path = (
DottedFieldPath(manager.field_id).child(param_info.name)
if manager.field_id
else DottedFieldPath(param_info.name)
)
owner_path = manager._object_state_path_for_field(param_info.name)
base_type = get_base_type_for_lazy(type(current_value)) or param_info.type
return cls(
state=manager.state,
Expand Down Expand Up @@ -106,7 +102,9 @@ def child_has_inherited_preview(self, field_name: str) -> bool:
def reset_child(self, field_name: str) -> None:
identity = self.child_identity(field_name)
container_value = self.state.parameters[self.owner_path.value]
default_value = self.state.signature_default(identity.object_state_path.value)
default_value = self.state.parameter_structure.declarations[
identity.object_state_path.value
].default_value
if self.raw_child_value(field_name) == default_value:
return
updated_value = replace_raw(container_value, **{field_name: default_value})
Expand All @@ -124,7 +122,9 @@ def reset_children(self) -> None:
for dataclass_field in dataclass_fields(self.owner_type):
field_name = dataclass_field.name
identity = self.child_identity(field_name)
default_value = self.state.signature_default(identity.object_state_path.value)
default_value = self.state.parameter_structure.declarations[
identity.object_state_path.value
].default_value
if self.raw_child_value(field_name) != default_value:
updates[field_name] = default_value

Expand Down
56 changes: 17 additions & 39 deletions src/pyqt_reactive/forms/parameter_form_chrome_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def changed_field_visuals(
param_name,
changed_paths,
refresh_compound_semantics=(
self._owner_path(param_name).value not in refreshed_compound_owner_paths
DottedFieldPath(self.manager.field_id).child(param_name).value not in refreshed_compound_owner_paths
),
)
self.update_owning_groupbox_dirty_marker()
Expand Down Expand Up @@ -107,7 +107,7 @@ def update_label_styling(
manager_field_id=manager.field_id,
param_name=param_name,
):
dotted_path = self._owner_path(param_name)
dotted_path = DottedFieldPath(manager.field_id).child(param_name)
should_underline = dotted_path.contains_any(manager.state.signature_diff_fields)
is_dirty = dotted_path.contains_any(manager.state.dirty_fields)

Expand All @@ -125,12 +125,6 @@ def update_label_styling(
self._compound_child_owner_paths(param_name, changed_paths),
)

def _owner_path(self, param_name: str) -> DottedFieldPath:
manager = self.manager
return DottedFieldPath(
f"{manager.field_id}.{param_name}" if manager.field_id else param_name
)

def _compound_child_owner_paths(
self,
param_name: str,
Expand All @@ -139,7 +133,7 @@ def _compound_child_owner_paths(
if changed_paths is None:
return None

owner_path = self._owner_path(param_name)
owner_path = DottedFieldPath(self.manager.field_id).child(param_name)
child_paths: list[DottedFieldPath] = []
seen: set[str] = set()
for changed_path in changed_paths:
Expand Down Expand Up @@ -210,22 +204,19 @@ def state_changed_for_paths(
else set()
)
refreshed_fields: set[str] = set()
manager_path = DottedFieldPath(manager.field_id)

for path in paths:
owner_field = self._direct_child_field_for_path(path)
field_path = DottedFieldPath(path)
owner_field = manager_path.direct_child_name(field_path)
if owner_field is not None:
refreshed_fields.add(owner_field)

if "." in path:
path_prefix, leaf_field = path.rsplit(".", 1)
else:
path_prefix = ""
leaf_field = path
if path_prefix == manager.field_id:
refreshed_fields.add(leaf_field)
if field_path.parent == manager_path:
refreshed_fields.add(field_path.field_name)

for param_name in refreshed_fields:
owner_path = self._owner_path(param_name).value
owner_path = DottedFieldPath(manager.field_id).child(param_name).value
refresh_compound_semantics = owner_path not in refreshed_compound_owner_paths
refreshed_compound_owner_paths.add(owner_path)
self.update_label_styling(
Expand Down Expand Up @@ -280,7 +271,7 @@ def refresh_widgets_from_state(self) -> None:
manager = self.manager
for param_name, widget in manager.widgets.items():
if isinstance(widget, ValueSettable):
dotted_path = f"{manager.field_id}.{param_name}" if manager.field_id else param_name
dotted_path = DottedFieldPath(manager.field_id).child(param_name).value
if dotted_path in manager.state.parameters:
value = manager.state.parameters.get(dotted_path)
manager._widget_service.update_widget_value(widget, value, param_name, False, manager)
Expand All @@ -298,6 +289,7 @@ def refresh_widgets_for_paths(self, paths: Set[str]) -> set[str]:
from objectstate.time_travel_profile import TimeTravelProfiler

manager = self.manager
manager_path = DottedFieldPath(manager.field_id)
missing = object()
refreshed_container_paths: set[str] = set()
refreshed_compound_owner_paths: set[str] = set()
Expand All @@ -309,19 +301,10 @@ def refresh_widgets_for_paths(self, paths: Set[str]) -> set[str]:
widgets=len(manager.widgets),
):
for path in paths:
if "." in path:
path_prefix, leaf_field = path.rsplit(".", 1)
else:
path_prefix = ""
leaf_field = path

owner_field = self._direct_child_field_for_path(path)
field_path = DottedFieldPath(path)
owner_field = manager_path.direct_child_name(field_path)
if owner_field is not None:
owner_path = (
f"{manager.field_id}.{owner_field}"
if manager.field_id
else owner_field
)
owner_path = manager_path.child(owner_field).value
if owner_path != path and owner_path not in refreshed_container_paths:
owner_widget = manager.widgets.get(owner_field)
raw_value = manager.state.parameters.get(owner_path, missing)
Expand Down Expand Up @@ -379,7 +362,8 @@ def refresh_widgets_for_paths(self, paths: Set[str]) -> set[str]:
refreshed_container_paths.add(owner_path)
refreshed_compound_owner_paths.add(owner_path)

if path_prefix == manager.field_id:
if field_path.parent == manager_path:
leaf_field = field_path.field_name
widget = manager.widgets.get(leaf_field)
if isinstance(widget, ValueSettable):
value = manager.state.parameters.get(path, missing)
Expand Down Expand Up @@ -411,14 +395,8 @@ def refresh_widgets_for_paths(self, paths: Set[str]) -> set[str]:

return refreshed_compound_owner_paths

def _direct_child_field_for_path(self, path: str) -> str | None:
return self.manager.form_tree.direct_child_field_for_path(
self.manager.field_id,
path,
)

def dispatch_reset(self, param_name: str) -> None:
dotted_path = f"{self.manager.field_id}.{param_name}" if self.manager.field_id else param_name
dotted_path = DottedFieldPath(self.manager.field_id).child(param_name).value
reset_value = self.manager.state.parameters.get(dotted_path)
event = FieldChangeEvent(param_name, reset_value, self.manager, is_reset=True)
FieldChangeDispatcher.instance().dispatch(event)
Loading
Loading