From 2e5158de24ced801fa732cc648f97dc92b42dce4 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Tue, 28 Apr 2026 08:33:06 +0200 Subject: [PATCH 1/3] (re)Implementation of PR3444 --- pyqtgraph/examples/parametertree.py | 23 ++ pyqtgraph/parametertree/__init__.py | 8 + pyqtgraph/parametertree/iojson.py | 245 ++++++++++++++++++ .../parametertree/parameterTypes/calendar.py | 6 +- pyqtgraph/parametertree/utils.py | 179 +++++++++++++ tests/parametertree/test_parametertypes.py | 162 ++++++++++++ 6 files changed, 621 insertions(+), 2 deletions(-) create mode 100644 pyqtgraph/parametertree/iojson.py create mode 100644 pyqtgraph/parametertree/utils.py diff --git a/pyqtgraph/examples/parametertree.py b/pyqtgraph/examples/parametertree.py index c5fa8ee6d4..b7648de907 100644 --- a/pyqtgraph/examples/parametertree.py +++ b/pyqtgraph/examples/parametertree.py @@ -17,6 +17,10 @@ app = pg.mkQApp("Parameter Tree Example") import pyqtgraph.parametertree.parameterTypes as pTypes from pyqtgraph.parametertree import Parameter, ParameterTree +from pyqtgraph.parametertree.iojson import ( + parameter_restore_from_json_file, + parameter_to_json_file, +) ## test subclassing parameters @@ -69,6 +73,8 @@ def addNew(self, typ): {'name': 'Add missing items', 'type': 'bool', 'value': True}, {'name': 'Remove extra items', 'type': 'bool', 'value': True}, ]}, + {'name': 'Save to JSON', 'type': 'action'}, + {'name': 'Restore from JSON', 'type': 'action'}, ]}, {'name': 'Custom context menu', 'type': 'group', 'children': [ {'name': 'List contextMenu', 'type': 'float', 'value': 0, 'context': [ @@ -124,8 +130,25 @@ def restore(): add = p['Save/Restore functionality', 'Restore State', 'Add missing items'] rem = p['Save/Restore functionality', 'Restore State', 'Remove extra items'] p.restoreState(state, addChildren=add, removeChildren=rem) + +def saveJson(): + path, _ = QtWidgets.QFileDialog.getSaveFileName( + None, 'Save parameters to JSON', '', 'JSON files (*.json)' + ) + if path: + parameter_to_json_file(p, path) + +def restoreJson(): + path, _ = QtWidgets.QFileDialog.getOpenFileName( + None, 'Load parameters from JSON', '', 'JSON files (*.json)' + ) + if path: + parameter_restore_from_json_file(p, path) + p.param('Save/Restore functionality', 'Save State').sigActivated.connect(save) p.param('Save/Restore functionality', 'Restore State').sigActivated.connect(restore) +p.param('Save/Restore functionality', 'Save to JSON').sigActivated.connect(saveJson) +p.param('Save/Restore functionality', 'Restore from JSON').sigActivated.connect(restoreJson) ## Create two ParameterTree widgets, both accessing the same data diff --git a/pyqtgraph/parametertree/__init__.py b/pyqtgraph/parametertree/__init__.py index 44b758d04b..a276376511 100644 --- a/pyqtgraph/parametertree/__init__.py +++ b/pyqtgraph/parametertree/__init__.py @@ -1,4 +1,12 @@ from . import parameterTypes as types +from .iojson import ( + parameter_from_json, + parameter_from_json_file, + parameter_restore_from_json, + parameter_restore_from_json_file, + parameter_to_json, + parameter_to_json_file, +) from .Parameter import Parameter, registerParameterItemType, registerParameterType from .ParameterItem import ParameterItem from .ParameterSystem import ParameterSystem, SystemSolver diff --git a/pyqtgraph/parametertree/iojson.py b/pyqtgraph/parametertree/iojson.py new file mode 100644 index 0000000000..267ede317d --- /dev/null +++ b/pyqtgraph/parametertree/iojson.py @@ -0,0 +1,245 @@ +"""JSON serialization for pyqtgraph Parameter trees. + +Intended as a companion to ``saveState()`` / ``restoreState()``: the same +round-trip guarantee, stored as a human-readable JSON file instead of an +in-memory dict. + +Design +------ +``saveState()`` is the canonical serializer. Every built-in Parameter +subclass already overrides it to return JSON-primitive values:: + + ColorParameter → (r, g, b, a) tuple + FontParameter → font-description string + CalendarParameter → date string + PenParameter → tuple of (color, width, style, capStyle, joinStyle, cosmetic) + QtEnumParameter → enum-member name string + +This module only needs to handle the small set of types that ``saveState()`` +still emits but that the standard ``json`` module cannot encode — see +:class:`~pyqtgraph.parametertree.utils.ParameterJsonEncoder` for the full list. + +Two workflows +------------- +**Full-structure round-trip** — clone or migrate a tree: + + json_str = parameter_to_json(param) + clone = parameter_from_json(json_str) + +**User-settings workflow** — save only user-modified values, reload into an +existing tree while preserving widget connections and signal handlers: + + # Save (only values the user changed) + parameter_to_json_file(param, 'settings.json', filter='user') + + # Load back into the same tree later + parameter_restore_from_json_file(param, 'settings.json') + +Public API +---------- +``parameter_to_json(param, indent=None, filter=None)`` → ``str`` +``parameter_from_json(json_str)`` → ``Parameter`` +``parameter_restore_from_json(param, json_str)`` +``parameter_to_json_file(param, path, *, overwrite=True, indent=2, encoding=None, filter=None)`` +``parameter_from_json_file(path, encoding=None)`` → ``Parameter`` +``parameter_restore_from_json_file(param, path, encoding=None)`` +""" +from __future__ import annotations + +import json +from pathlib import Path +from typing import Literal, Union + +from .Parameter import Parameter +from .utils import ParameterJsonEncoder, _decode_hook + +__all__ = [ + 'parameter_to_json', + 'parameter_from_json', + 'parameter_restore_from_json', + 'parameter_to_json_file', + 'parameter_from_json_file', + 'parameter_restore_from_json_file', +] + +# RFC 8259 mandates UTF-8 for JSON interchange; used as the default when no +# encoding is explicitly requested. +_JSON_ENCODING = 'utf-8' + +_Filter = Literal['user'] | None + + +# --------------------------------------------------------------------------- +# String API +# --------------------------------------------------------------------------- + +def parameter_to_json( + param: Parameter, + indent: int | None = None, + filter: _Filter = None, +) -> str: + """Serialize a Parameter tree to a JSON string. + + Calls :meth:`~pyqtgraph.parametertree.Parameter.saveState` internally so + that each Parameter subclass's own ``saveState()`` override handles Qt- + object conversion before the encoder sees the data. + + Parameters + ---------- + param: + Root of the Parameter tree (or any sub-tree node). + indent: + ``None`` (default) produces a compact single-line string; an integer + such as ``2`` produces human-readable indented output. + filter: + Passed to ``saveState()``. ``None`` (default) saves the full tree + structure including type, limits, and all opts — suitable for cloning + or migrating a tree. ``'user'`` saves only user-settable values, + producing a compact settings file that can be reloaded into an + existing tree via :func:`parameter_restore_from_json`. + + Returns + ------- + str + A JSON string representing the parameter tree. + """ + return ParameterJsonEncoder(indent=indent).encode(param.saveState(filter=filter)) + + +def parameter_from_json(json_str: str) -> Parameter: + """Reconstruct a Parameter tree from a full-structure JSON string. + + Parameters + ---------- + json_str: + A JSON string produced by :func:`parameter_to_json` (with + ``filter=None``) or read from a file written by + :func:`parameter_to_json_file`. + + Returns + ------- + Parameter + A fully reconstructed Parameter tree. + + See Also + -------- + parameter_restore_from_json : load user-settings into an existing tree. + """ + state = json.loads(json_str, object_hook=_decode_hook) + return Parameter.create(**state) + + +def parameter_restore_from_json(param: Parameter, json_str: str) -> None: + """Restore parameter values from a JSON string into an *existing* tree. + + Uses :meth:`~pyqtgraph.parametertree.Parameter.restoreState` so that all + widget connections and signal handlers on *param* are preserved. Intended + for loading user settings saved with ``filter='user'``, but also works + with full-structure JSON. + + Parameters + ---------- + param: + The existing Parameter tree to update. + json_str: + A JSON string produced by :func:`parameter_to_json`. + """ + state = json.loads(json_str, object_hook=_decode_hook) + param.restoreState(state) + + +# --------------------------------------------------------------------------- +# File API +# --------------------------------------------------------------------------- + +def parameter_to_json_file( + param: Parameter, + path: Union[str, Path], + *, + overwrite: bool = True, + indent: int = 2, + encoding: str | None = None, + filter: _Filter = None, +) -> None: + """Serialize a Parameter tree to a ``.json`` file. + + Parameters + ---------- + param: + Root of the Parameter tree (or any sub-tree node). + path: + Destination path. The ``.json`` extension is enforced regardless of + whatever suffix *path* already carries. + overwrite: + When ``False`` and the destination file already exists, raises + :exc:`FileExistsError` instead of overwriting. + indent: + Indentation used in the output file (default ``2``). Pass ``None`` + for a compact single-line file. + encoding: + Text encoding for the file. Defaults to ``'utf-8'`` (RFC 8259). + filter: + Passed to ``saveState()``. ``'user'`` saves only user-settable values + (recommended for settings files); ``None`` saves the full structure. + """ + path = Path(path).with_suffix('.json') + if not overwrite and path.exists(): + raise FileExistsError(f"{path} already exists") + path.write_text( + ParameterJsonEncoder(indent=indent).encode(param.saveState(filter=filter)), + encoding=encoding or _JSON_ENCODING, + ) + + +def parameter_from_json_file( + path: Union[str, Path], + encoding: str | None = None, +) -> Parameter: + """Reconstruct a Parameter tree from a full-structure ``.json`` file. + + Parameters + ---------- + path: + Path to the ``.json`` file written by :func:`parameter_to_json_file` + with ``filter=None``. + encoding: + Text encoding of the file. Defaults to ``'utf-8'`` (RFC 8259). + + Returns + ------- + Parameter + A fully reconstructed Parameter tree. + + See Also + -------- + parameter_restore_from_json_file : load user-settings into an existing tree. + """ + text = Path(path).read_text(encoding=encoding or _JSON_ENCODING) + state = json.loads(text, object_hook=_decode_hook) + return Parameter.create(**state) + + +def parameter_restore_from_json_file( + param: Parameter, + path: Union[str, Path], + encoding: str | None = None, +) -> None: + """Restore parameter values from a ``.json`` file into an *existing* tree. + + Uses :meth:`~pyqtgraph.parametertree.Parameter.restoreState` so that all + widget connections and signal handlers on *param* are preserved. Intended + for loading user settings saved with ``filter='user'``, but also works + with full-structure JSON. + + Parameters + ---------- + param: + The existing Parameter tree to update. + path: + Path to the ``.json`` file written by :func:`parameter_to_json_file`. + encoding: + Text encoding of the file. Defaults to ``'utf-8'`` (RFC 8259). + """ + text = Path(path).read_text(encoding=encoding or _JSON_ENCODING) + state = json.loads(text, object_hook=_decode_hook) + param.restoreState(state) diff --git a/pyqtgraph/parametertree/parameterTypes/calendar.py b/pyqtgraph/parametertree/parameterTypes/calendar.py index ae0b134871..836b8d9d76 100644 --- a/pyqtgraph/parametertree/parameterTypes/calendar.py +++ b/pyqtgraph/parametertree/parameterTypes/calendar.py @@ -51,6 +51,8 @@ def _interpretValue(self, v): def saveState(self, filter=None): state = super().saveState(filter) fmt = self._interpretFormat() - if state.get('value', None) is not None: - state['value'] = state['value'].toString(fmt) + for key in ('value', 'default'): + val = state.get(key) + if isinstance(val, QtCore.QDate): + state[key] = val.toString(fmt) return state diff --git a/pyqtgraph/parametertree/utils.py b/pyqtgraph/parametertree/utils.py new file mode 100644 index 0000000000..0e09fb1427 --- /dev/null +++ b/pyqtgraph/parametertree/utils.py @@ -0,0 +1,179 @@ +"""JSON serialization utilities for pyqtgraph Parameter trees. + +Design notes +------------ +``saveState()`` is the canonical serializer: every built-in Parameter subclass +already overrides it to return JSON-primitive values (e.g. ``ColorParameter`` +→ RGBA tuple, ``FontParameter`` → string, ``CalendarParameter`` → string, +``PenParameter`` → tuple of primitives, ``QtEnumParameter`` → string name). +This module only needs to handle the residual types that ``saveState()`` still +emits but that the standard ``json`` module cannot encode: + +- **tuple** — JSON has no tuple type; plain ``json`` silently converts tuples + to lists, which breaks ``PenParameter`` (``mkPen`` checks + ``isinstance(v, tuple)``) and any parameter using tuple-valued opts such as + ``limits``. Tuples are preserved with a ``{"__tuple__": [...]}`` sentinel. + +- **numpy ndarray** — encoded as ``{"__ndarray__": [...], "dtype": ""}`` + so that shape and element type survive a round-trip. + +- **numpy scalar types** (``np.integer``, ``np.floating``, ``np.bool_``) — + silently promoted to the matching Python primitive; no sentinel needed. + +- **ColorMap** — two forms depending on whether the map is named: + + 1. *Named colormaps* (``colormap.name`` is set) are stored as + ``{"__colormap__": {"name": ""}}``, reconstructed via + ``pyqtgraph.colormap.get()``. A viridis LUT shrinks from ~5 kB to a + few bytes. + 2. *Anonymous colormaps* fall back to the full + ``{"__colormap__": {pos, color, mapping_mode, name}}`` form. + +If a ``saveState()`` override returns any other non-serializable object (e.g. +a ``QDate`` from a custom parameter type that forgot to convert it), the +encoder raises ``TypeError`` as usual. The correct fix is always in the +parameter type's ``saveState()``, not here. + +Public helpers +-------------- +``ParameterJsonEncoder`` — ``JSONEncoder`` subclass; use with ``json.dumps`` +``_decode_hook`` — ``object_hook`` for ``json.loads`` +""" +from __future__ import annotations + +import json +from json import JSONDecoder, JSONEncoder +from typing import Any + +import numpy as np + +from ..colormap import ColorMap +import pyqtgraph.colormap as pgcm + + +# --------------------------------------------------------------------------- +# Pre-encoding helper: mark tuples before json.dumps sees the structure +# --------------------------------------------------------------------------- + +def _mark_special(o: Any) -> Any: + """Recursively replace every ``tuple`` with ``{"__tuple__": [...]}``. + + This traversal happens *before* ``JSONEncoder.encode`` so that the + sentinel dicts are visible to the standard encoder. ``dict`` values and + ``list`` elements are also walked so that tuples nested at any depth are + found. Non-container objects (including non-serializable ones like numpy + arrays) are returned unchanged and will later hit ``default()``. + """ + if isinstance(o, tuple): + return {'__tuple__': [_mark_special(e) for e in o]} + if isinstance(o, dict): + return {k: _mark_special(v) for k, v in o.items()} + if isinstance(o, list): + return [_mark_special(e) for e in o] + return o + + +# --------------------------------------------------------------------------- +# Decoding hook +# --------------------------------------------------------------------------- + +def _decode_hook(dct: dict) -> Any: + """``object_hook`` for :func:`json.loads`. + + The JSON decoder calls this bottom-up (innermost dicts first), so nested + sentinels are already reconstructed when an outer sentinel is reached — + no explicit recursion is required here. + + Reconstructs: + + - ``{"__tuple__": [...]}`` → ``tuple`` + - ``{"__ndarray__": [...], ...}`` → ``numpy.ndarray`` + - ``{"__colormap__": {"name": ...}}``→ named :class:`~pyqtgraph.ColorMap` + - ``{"__colormap__": {...}}`` → anonymous :class:`~pyqtgraph.ColorMap` + """ + if '__tuple__' in dct: + # Elements may already be reconstructed tuples/arrays from inner calls + return tuple(dct['__tuple__']) + + if '__ndarray__' in dct: + arr = np.array(dct['__ndarray__']) + dtype = dct.get('dtype') + if dtype: + arr = arr.astype(dtype, copy=False) + return arr + + if '__colormap__' in dct: + attrs = dct['__colormap__'] + name = attrs.get('name') + if name and 'pos' not in attrs: + # Named colormap — reconstruct from the registry + return pgcm.get(name) + # pos and color have already been decoded as ndarrays by this point + return ColorMap( + pos=attrs['pos'], + color=attrs['color'], + mapping=attrs['mapping_mode'], + name=name, + ) + + return dct + + +# --------------------------------------------------------------------------- +# Encoder +# --------------------------------------------------------------------------- + +class ParameterJsonEncoder(JSONEncoder): + """``JSONEncoder`` subclass for pyqtgraph Parameter tree serialization. + + Usage:: + + json_str = ParameterJsonEncoder(indent=2).encode(param.saveState()) + state = json.loads(json_str, object_hook=_decode_hook) + + The encoder handles the small set of types that ``saveState()`` may still + emit after each Parameter type has already converted its Qt objects to + primitives. See the module docstring for the full list. + """ + + def encode(self, o: Any) -> str: + """Pre-process *o* to mark tuples, then delegate to the standard encoder.""" + return super().encode(_mark_special(o)) + + def iterencode(self, o: Any, _one_shot: bool = False): + """Pre-process *o* to mark tuples before chunk-based encoding (used by ``indent``).""" + return super().iterencode(_mark_special(o), _one_shot) + + def default(self, o: Any) -> Any: + """Handle non-serializable objects not covered by ``saveState()`` overrides.""" + if isinstance(o, np.ndarray): + return {'__ndarray__': o.tolist(), 'dtype': str(o.dtype)} + + # numpy scalar types — promote to Python primitives, no sentinel needed + if isinstance(o, np.integer): + return int(o) + if isinstance(o, np.floating): + return float(o) + if isinstance(o, np.bool_): + return bool(o) + + if isinstance(o, ColorMap): + if o.name: + # Named colormaps: store only the name — far more compact + return {'__colormap__': {'name': o.name}} + pos, color = o.getStops() + # pos and color are ndarrays; they will re-enter default() below + return { + '__colormap__': { + 'pos': pos, + 'color': color, + 'mapping_mode': o.mapping_mode, + 'name': o.name, + } + } + + return super().default(o) + + def decode(self, json_str: str) -> Any: + """Decode a JSON string produced by this encoder back to Python objects.""" + return JSONDecoder(object_hook=_decode_hook).decode(json_str) diff --git a/tests/parametertree/test_parametertypes.py b/tests/parametertree/test_parametertypes.py index 4dbbf957cd..66c3556bf1 100644 --- a/tests/parametertree/test_parametertypes.py +++ b/tests/parametertree/test_parametertypes.py @@ -234,3 +234,165 @@ def test_recreate_from_savestate(): state = created.saveState() created2 = pt.Parameter.create(**state) assert pg.eq(state, created2.saveState()) + + +# --------------------------------------------------------------------------- +# JSON serialization tests +# +# The fixture mirrors test_recreate_from_savestate: makeAllParamTypes() builds +# the same parameter tree used in examples/parametertree.py, so JSON behaviour +# is validated against the canonical example rather than ad-hoc specs. +# Targeted tests (pen/color/limits) exercise specific encoder edge cases that +# the full-tree test would catch but not diagnose clearly on failure. +# --------------------------------------------------------------------------- + +import json as _json + +import pytest + +from pyqtgraph.parametertree.iojson import ( + parameter_from_json, + parameter_from_json_file, + parameter_restore_from_json, + parameter_restore_from_json_file, + parameter_to_json, + parameter_to_json_file, +) + + +@pytest.fixture +def example_params(): + """Full parameter tree from examples/parametertree.py.""" + from pyqtgraph.examples import _buildParamTypes + return _buildParamTypes.makeAllParamTypes() + + +# --- round-trip correctness -------------------------------------------------- + +def test_json_round_trip(example_params): + """Full example tree survives a JSON string round-trip unchanged.""" + original_state = example_params.saveState() + restored = parameter_from_json(parameter_to_json(example_params)) + assert pg.eq(original_state, restored.saveState()) + + +def test_json_file_round_trip(example_params, tmp_path): + """Full example tree survives a JSON file round-trip unchanged.""" + original_state = example_params.saveState() + dest = tmp_path / 'params.json' + parameter_to_json_file(example_params, dest) + assert dest.exists() + restored = parameter_from_json_file(dest) + assert pg.eq(original_state, restored.saveState()) + + +# --- encoder edge cases ------------------------------------------------------ + +def test_json_tuple_preservation(): + """Tuples survive as tuples, not lists. + + PenParameter.mkPen() checks isinstance(v, tuple) to detect its serialized + form, so silently converting tuples to lists breaks pen restoration. + """ + pen = pt.Parameter.create(name='pen', type='pen') + original_state = pen.saveState() + assert isinstance(original_state['value'], tuple), \ + "PenParameter.saveState() must return a tuple value" + + restored = parameter_from_json(parameter_to_json(pen)) + restored_state = restored.saveState() + assert isinstance(restored_state['value'], tuple), \ + "Tuple value must be preserved through JSON round-trip" + assert pg.eq(original_state, restored_state) + + +def test_json_color_parameter(): + """Color parameter (QColor → RGBA tuple via saveState) round-trips correctly.""" + p = pt.Parameter.create(name='c', type='color', value='#ff8800') + assert pg.eq(p.saveState(), parameter_from_json(parameter_to_json(p)).saveState()) + + +def test_json_limits_tuple(): + """Tuple-valued opts such as ``limits`` survive as tuples.""" + p = pt.Parameter.create(name='root', type='group', children=[ + dict(name='x', type='float', value=1.0, limits=(0.0, 10.0)), + ]) + restored = parameter_from_json(parameter_to_json(p)) + assert isinstance(restored.saveState()['children']['x'].get('limits'), tuple) + + +# --- filter='user' and restore path ------------------------------------------ + +def test_json_filter_user(example_params): + """filter='user' produces a compact values-only file; restore updates the tree.""" + float_child = example_params.child('Sample Float').child('widget') + float_child.setValue(3.14) + + user_json = parameter_to_json(example_params, filter='user') + user_state = _json.loads(user_json) + + # User-filtered state carries no 'type', 'limits', etc. — only values + assert 'type' not in user_state + + # Restore into a fresh copy and verify the changed value came through + fresh = _buildParamTypes_makeAllParamTypes() + parameter_restore_from_json(fresh, user_json) + assert fresh.child('Sample Float').child('widget').value() == pytest.approx(3.14) + + +def test_json_restore_preserves_signals(example_params): + """restoreState path fires signals but keeps existing connections alive.""" + float_child = example_params.child('Sample Float').child('widget') + float_child.setValue(1.0) + + received = [] + float_child.sigValueChanged.connect(lambda p, v: received.append(v)) + + # Capture state at 1.0, mutate to 9.0, then restore — signal must fire + json_str = parameter_to_json(example_params) + float_child.setValue(9.0) + parameter_restore_from_json(example_params, json_str) + + assert float_child.value() == pytest.approx(1.0) + assert len(received) >= 1, "sigValueChanged must fire through restoreState" + + +def test_json_restore_from_file(example_params, tmp_path): + """parameter_restore_from_json_file updates an existing tree from a file.""" + float_child = example_params.child('Sample Float').child('widget') + float_child.setValue(2.71) + + parameter_to_json_file(example_params, tmp_path / 'settings', filter='user') + + fresh = _buildParamTypes_makeAllParamTypes() + parameter_restore_from_json_file(fresh, tmp_path / 'settings.json') + assert fresh.child('Sample Float').child('widget').value() == pytest.approx(2.71) + + +# --- overwrite guard --------------------------------------------------------- + +def test_json_file_no_overwrite(example_params, tmp_path): + """parameter_to_json_file raises FileExistsError when overwrite=False.""" + dest = tmp_path / 'params.json' + parameter_to_json_file(example_params, dest) + with pytest.raises(FileExistsError): + parameter_to_json_file(example_params, dest, overwrite=False) + + +# --- public API surface ------------------------------------------------------- + +def test_json_accessible_from_package(): + """All six functions are importable directly from pyqtgraph.parametertree.""" + import pyqtgraph.parametertree as ptt + for name in ( + 'parameter_to_json', 'parameter_from_json', 'parameter_restore_from_json', + 'parameter_to_json_file', 'parameter_from_json_file', + 'parameter_restore_from_json_file', + ): + assert callable(getattr(ptt, name)) + + +# helper — avoids repeating the import in every test body +def _buildParamTypes_makeAllParamTypes(): + from pyqtgraph.examples import _buildParamTypes + return _buildParamTypes.makeAllParamTypes() From 73cdf4b2d6695a84b942f5f8df1c9ef18462d2c4 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Tue, 28 Apr 2026 08:53:43 +0200 Subject: [PATCH 2/3] removing unused import --- pyqtgraph/parametertree/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/parametertree/utils.py b/pyqtgraph/parametertree/utils.py index 0e09fb1427..4dcea00047 100644 --- a/pyqtgraph/parametertree/utils.py +++ b/pyqtgraph/parametertree/utils.py @@ -41,7 +41,6 @@ """ from __future__ import annotations -import json from json import JSONDecoder, JSONEncoder from typing import Any From 6affc331b96d317f1f697b1d3ecd6032add079de Mon Sep 17 00:00:00 2001 From: Ashwola Date: Tue, 28 Apr 2026 14:49:26 +0200 Subject: [PATCH 3/3] fixing nesting structures and repeated entries --- pyqtgraph/examples/parametertree.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/pyqtgraph/examples/parametertree.py b/pyqtgraph/examples/parametertree.py index 5996239715..62f517a03c 100644 --- a/pyqtgraph/examples/parametertree.py +++ b/pyqtgraph/examples/parametertree.py @@ -132,21 +132,6 @@ def addNew(self, typ=None): params = [ makeAllParamTypes(), - { - "name": "Save/Restore functionality", - "type": "group", - "children": [ - {"name": "Save State", "type": "action"}, - { - "name": "Restore State", - "type": "action", - "children": [ - {"name": "Add missing items", "type": "bool", "value": True}, - {"name": "Remove extra items", "type": "bool", "value": True}, - ], - }, - ], - }, { "name": "Custom context menu", "type": "group", @@ -198,6 +183,7 @@ def addNew(self, typ=None): {'name': 'Extra context actions', 'type': 'int', 'value': 0, 'context': {'log': 'Print value to console'}, 'tip': 'User-defined context actions appear in the Manage section'}, + ]}, {'name': 'Icon Examples', 'type': 'group', 'expanded':False, 'children': [ {'name': 'Single parameter with icon', 'type': 'int', 'value': 42, 'icon': QtWidgets.QStyle.StandardPixmap.SP_ComputerIcon}, {'name': 'Group with icon', 'type': 'group', 'icon': QtWidgets.QStyle.StandardPixmap.SP_DirOpenIcon, 'children': [ @@ -213,7 +199,6 @@ def addNew(self, typ=None): ]}, ]}, {'name': 'Save/Restore functionality', 'type': 'group', 'children': [ - {'name': 'functionality', 'type': 'bool', 'value': True}, {'name': 'Save State', 'type': 'action'}, {'name': 'Restore State', 'type': 'action', 'children': [ {'name': 'Add missing items', 'type': 'bool', 'value': True}, @@ -232,8 +217,6 @@ def addNew(self, typ=None): ], ), ] -} -] ## Create tree of Parameter objects p = Parameter.create(name="params", type="group", children=params)