diff --git a/doc/release_notes/release_3.16.md b/doc/release_notes/release_3.16.md
index a45fb2b..86dac3e 100644
--- a/doc/release_notes/release_3.16.md
+++ b/doc/release_notes/release_3.16.md
@@ -1,18 +1,5 @@
# Version 3.16 #
-## Unreleased ##
-
-* Python 3.10 or later is now required. Python 3.9 users must keep an earlier compatible release.
-
-⨠New features:
-
-* **`get_pixmap` â sharp images with display scaling** â the new `guidata.configtools.get_pixmap` function loads an image as a pixmap at the resolution of the display: when the display scaling is greater than 100% (e.g. 125%, 150%), the high-resolution variants of the image following Qt's `@Nx` naming convention (e.g. `logo@2x.png` next to `logo.png`) are used, so that the image remains sharp instead of being upscaled. `get_image_label` (and thus `get_image_layout`) now relies on it.
-
-đ Bug fixes:
-
-* **Text not scaled with display scaling on Windows (Qt5)** â with a Windows display scaling greater than 100% (e.g. 125%), the text of most widgets (labels, buttons, data set editors, ...) kept the size it has at 100%, while menus and status bars were scaled, because Qt5 derives its default font from a legacy system font which ignores the display scaling. Applications created with `guidata.qapplication` (or `qt_app_context`) now use the message font of the system as default font (typically "Segoe UI", 9pt), like with Qt6: the text follows the display scaling and the Windows text size setting.
-* **Application startup blocked by a corrupted translation file** â when a `.mo` translation catalog was corrupted or unreadable (e.g. truncated during installation), importing guidata failed with an error such as `OSError: Bad magic number`. The same applied to any application relying on `guidata.configtools.get_translation`, like PlotPy, Sigima or DataLab, so the application could not start at all. The interface now falls back to English, and a `RuntimeWarning` identifies the faulty catalog (fixes [Issue #119](https://github.com/PlotPyStack/guidata/issues/119)).
-
## guidata Version 3.16.0 ##
⨠New features:
diff --git a/doc/release_notes/release_3.17.md b/doc/release_notes/release_3.17.md
new file mode 100644
index 0000000..368d33d
--- /dev/null
+++ b/doc/release_notes/release_3.17.md
@@ -0,0 +1,22 @@
+# Version 3.17 #
+
+## guidata Version 3.17.0 ##
+
+* Python 3.10 or later is now required. Python 3.9 users must keep an earlier compatible release.
+
+⨠New features:
+
+* **`get_pixmap` â sharp images with display scaling** â the new `guidata.configtools.get_pixmap` function loads an image as a pixmap at the resolution of the display: when the display scaling is greater than 100% (e.g. 125%, 150%), the high-resolution variants of the image following Qt's `@Nx` naming convention (e.g. `logo@2x.png` next to `logo.png`) are used, so that the image remains sharp instead of being upscaled. `get_image_label` (and thus `get_image_layout`) now relies on it.
+
+đ Bug fixes:
+
+* **Text not scaled with display scaling on Windows (Qt5)** â with a Windows display scaling greater than 100% (e.g. 125%), the text of most widgets (labels, buttons, data set editors, ...) kept the size it has at 100%, while menus and status bars were scaled, because Qt5 derives its default font from a legacy system font which ignores the display scaling. Applications created with `guidata.qapplication` (or `qt_app_context`) now use the message font of the system as default font (typically "Segoe UI", 9pt), like with Qt6: the text follows the display scaling and the Windows text size setting.
+* **Application startup blocked by a corrupted translation file** â when a `.mo` translation catalog was corrupted or unreadable (e.g. truncated during installation), importing guidata failed with an error such as `OSError: Bad magic number`. The same applied to any application relying on `guidata.configtools.get_translation`, like PlotPy, Sigima or DataLab, so the application could not start at all. The interface now falls back to English, and a `RuntimeWarning` identifies the faulty catalog (fixes [Issue #119](https://github.com/PlotPyStack/guidata/issues/119)).
+* **Error when editing a dictionary field in a JSON Schema based form** â in a form generated with `guidata.dataset.dataset_to_schema` (e.g. in a web frontend), editing a `DictItem` field failed with `TypeError: DictItem.__dictedit() missing 1 required positional argument: 'parent'`: the button opening the dictionary editor in the Qt interface was handled as a callback updating the other fields. `DictItem` fields are no longer flagged with `x-guidata-has-callback`, and `resolve_dataset_callbacks` returns an empty mapping for them (fixes [Issue #123](https://github.com/PlotPyStack/guidata/issues/123)).
+* **Date/time callback never called** â the `display` callback of a `DateTimeItem` was never called when the date or time was changed in a dataset editor, so that the values computed from it were not updated. The callback is now called on each change, with the new value of the item (a `datetime.datetime` object), like for the other items (fixes [Issue #123](https://github.com/PlotPyStack/guidata/issues/123)).
+* **Plain-text "about" information** â `guidata.widgets.about.about(html=False)` (and `AboutInfo.about(html=False)`) failed with `UnboundLocalError` instead of returning the information about the package as plain text (fixes [Issue #123](https://github.com/PlotPyStack/guidata/issues/123)).
+* **French translations of the DataFrame editor** â when entering a number format not starting with `%`, the French error message stated the opposite of the actual rule ("must not start with '%'"), and the title of the export dialog box was shown in English. Both are now correctly translated (fixes [Issue #123](https://github.com/PlotPyStack/guidata/issues/123)).
+
+âšī¸ Other changes:
+
+* **Documentation** â `guidata.dataset.resolve_dataset_active`, which evaluates the active state of the items of a dataset for JSON Schema based frontends, is now documented in the API reference.
diff --git a/guidata/__init__.py b/guidata/__init__.py
index e92f13d..7daaaba 100644
--- a/guidata/__init__.py
+++ b/guidata/__init__.py
@@ -8,7 +8,7 @@
and application development tools for Qt.
"""
-__version__ = "3.16.0"
+__version__ = "3.17.0"
# Dear (Debian, RPM, ...) package makers, please feel free to customize the
diff --git a/guidata/dataset/jsonschema.py b/guidata/dataset/jsonschema.py
index adbc505..8c8b508 100644
--- a/guidata/dataset/jsonschema.py
+++ b/guidata/dataset/jsonschema.py
@@ -30,6 +30,8 @@
.. autofunction:: resolve_dataset_callbacks
+.. autofunction:: resolve_dataset_active
+
Output format
-------------
@@ -84,7 +86,7 @@
import inspect
import mimetypes
import os
-from collections.abc import Collection
+from collections.abc import Callable, Collection
from typing import TYPE_CHECKING, Any
import guidata.dataset.dataitems as gdi
@@ -227,7 +229,7 @@ def _apply_display_callbacks(instance: gdt.DataSet) -> None:
"""
for item in instance.get_items():
try:
- callback = item.get_prop_value("display", instance, "callback", None)
+ callback = _get_reactive_callback(item, instance)
except Exception: # pylint: disable=broad-except
callback = None
if callback is None:
@@ -294,13 +296,14 @@ def resolve_dataset_callbacks(instance: gdt.DataSet, item_name: str) -> dict[str
Returns:
A JSON-serialisable mapping of property name â value for every
non-group item, reflecting the post-callback state. Empty when
- the item carries no ``display`` callback.
+ the item carries no reactive ``display`` callback (e.g. a
+ :class:`DictItem`, whose callback opens the Qt dictionary editor).
Raises:
KeyError: If *item_name* is not an item of *instance*.
"""
item = _get_item(instance, item_name)
- callback = item.get_prop_value("display", instance, "callback", None)
+ callback = _get_reactive_callback(item, instance)
if callback is None:
return {}
callback(instance, item, item.get_value(instance))
@@ -762,7 +765,7 @@ def _add_common_keys(item: gdt.DataItem, prop: dict[str, Any], order: int) -> No
# the read-only ``operation`` preview). Flag it so the frontend can
# round-trip through :func:`resolve_dataset_callbacks` on edit,
# mirroring the Qt ``_display_callback`` cascade.
- if item.get_prop("display", "callback", None) is not None:
+ if _get_reactive_callback(item) is not None:
prop["x-guidata-has-callback"] = True
prop["x-guidata-name"] = name
prop["x-guidata-order"] = order
@@ -841,3 +844,28 @@ def _get_item(instance: gdt.DataSet, name: str) -> gdt.DataItem:
if item.get_name() == name:
return item
raise KeyError(name)
+
+
+def _get_reactive_callback(
+ item: gdt.DataItem, instance: gdt.DataSet | None = None
+) -> Callable[..., Any] | None:
+ """Return the reactive ``display`` callback of *item*, if any.
+
+ A reactive callback has the ``callback(instance, item, value)`` signature
+ and recomputes sibling items when *item* changes. The ``display`` callback
+ of a :class:`DictItem` is not reactive: it opens the Qt dictionary editor
+ from a button, with a different signature, so it is ignored.
+
+ Args:
+ item: The data item.
+ instance: The :class:`DataSet` instance used to evaluate the callback
+ property, or ``None`` to return the property declared on *item*.
+
+ Returns:
+ The reactive callback, or ``None``.
+ """
+ if isinstance(item, gdi.DictItem):
+ return None
+ if instance is None:
+ return item.get_prop("display", "callback", None)
+ return item.get_prop_value("display", instance, "callback", None)
diff --git a/guidata/dataset/qtitemwidgets.py b/guidata/dataset/qtitemwidgets.py
index dc07df4..743d1c6 100644
--- a/guidata/dataset/qtitemwidgets.py
+++ b/guidata/dataset/qtitemwidgets.py
@@ -807,17 +807,16 @@ def __init__(
self.dateedit = self.group = QDateTimeEdit()
self.dateedit.setCalendarPopup(True)
self.dateedit.setToolTip(item.get_help())
- self.dateedit.dateTimeChanged.connect( # type:ignore
- lambda value: self.notify_value_change()
- )
+ self.dateedit.dateTimeChanged.connect(self.date_changed) # type:ignore
fmt = self.item.get_prop("display", "format", None)
if fmt:
qt_fmt = convert_date_format(fmt)
self.dateedit.setDisplayFormat(qt_fmt)
- def date_changed(self, value):
- """Date changed"""
- _display_callback(self, value)
+ def date_changed(self, _value) -> None:
+ """Date/time changed (the ``QDateTime`` argument is ignored: the callback
+ receives the Python value of the widget)"""
+ _display_callback(self, self.value())
self.notify_value_change()
def get(self) -> None:
diff --git a/guidata/locale/fr/LC_MESSAGES/guidata.po b/guidata/locale/fr/LC_MESSAGES/guidata.po
index f4dd025..b50687d 100644
--- a/guidata/locale/fr/LC_MESSAGES/guidata.po
+++ b/guidata/locale/fr/LC_MESSAGES/guidata.po
@@ -645,14 +645,13 @@ msgstr "Le format ({}) est incorrect"
#, python-brace-format
msgid "Format ({}) should start with '%'"
-msgstr "Le format ({}) ne doit pas commencer par '%'"
+msgstr "Le format ({}) doit commencer par '%'"
msgid "CSV Files"
msgstr "Fichiers CSV"
-#, fuzzy
msgid "Export dataframe"
-msgstr "DataFrame"
+msgstr "Exporter le DataFrame"
msgid "Import as"
msgstr "Importer en tant que"
diff --git a/guidata/tests/dataset/test_jsonschema.py b/guidata/tests/dataset/test_jsonschema.py
index 7dd63f0..b505c1b 100644
--- a/guidata/tests/dataset/test_jsonschema.py
+++ b/guidata/tests/dataset/test_jsonschema.py
@@ -681,3 +681,21 @@ class P(gds.DataSet):
# An item without a callback yields an empty mapping (no-op).
assert resolve_dataset_callbacks(P(), "preview") == {}
+
+
+def test_dict_item_editor_callback_is_not_reactive():
+ """The ``DictItem`` editor callback is not exposed as a reactive callback.
+
+ ``DictItem`` uses its ``display`` callback to open the Qt dictionary
+ editor (with a different signature): it must neither be flagged nor run
+ by the off-GUI callback resolution.
+ """
+
+ class P(gds.DataSet):
+ meta = gds.DictItem("Meta", default={"k": 1})
+
+ props = dataset_to_schema(P)["properties"]
+ assert "x-guidata-has-callback" not in props["meta"]
+ payload = dataset_to_schema_with_values(P())
+ assert payload["values"]["meta"] == {"k": 1}
+ assert resolve_dataset_callbacks(P(), "meta") == {}
diff --git a/guidata/tests/unit/test_about.py b/guidata/tests/unit/test_about.py
new file mode 100644
index 0000000..9e04693
--- /dev/null
+++ b/guidata/tests/unit/test_about.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed under the terms of the BSD 3-Clause
+# (see guidata/LICENSE for details)
+
+"""
+Test the text about a package
+
+``AboutInfo.about`` (and ``about`` for guidata) returns the text about a package,
+either as HTML (e.g. for the about dialog box) or as plain text.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+from guidata.widgets.about import AboutInfo, about
+
+
+def _get_info() -> AboutInfo:
+ """Return information about a test package"""
+ return AboutInfo(
+ "mypackage", "1.2.3", "My description", "John Doe", 2020, "MyOrganization"
+ )
+
+
+def test_about_plain_text() -> None:
+ """Plain-text output contains the package information without HTML"""
+ text = _get_info().about(html=False)
+ assert text.startswith("mypackage 1.2.3\nMy description\n")
+ for info in ("John Doe", "2020", "MyOrganization", "Copyright"):
+ assert info in text
+ assert "<" not in text
+
+
+def test_about_html() -> None:
+ """HTML output contains links to the author and the organization"""
+ text = _get_info().about(html=True)
+ assert "John Doe" in text
+ assert "MyOrganization" in text
+ assert "\n" not in text
+
+
+@pytest.mark.parametrize("html", [True, False])
+def test_guidata_about(html: bool) -> None:
+ """Text about guidata, complete or limited to the copyright"""
+ assert "Pierre Raybaut" in about(html=html)
+ text = about(html=html, copyright_only=True)
+ assert "Copyright" in text
+ assert "Pierre Raybaut" not in text
diff --git a/guidata/tests/unit/test_datetime_display_callback.py b/guidata/tests/unit/test_datetime_display_callback.py
new file mode 100644
index 0000000..09dec3b
--- /dev/null
+++ b/guidata/tests/unit/test_datetime_display_callback.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed under the terms of the BSD 3-Clause
+# (see guidata/LICENSE for details)
+
+"""
+Test the display callback of DateTimeItem
+
+When the date/time is changed in a dataset editor, the ``display`` callback of a
+``DateTimeItem`` must be called with the Python value of the item (``datetime``).
+"""
+
+from __future__ import annotations
+
+import datetime
+
+from qtpy import QtCore as QC
+from qtpy import QtWidgets as QW
+
+from guidata.dataset import DataSet
+from guidata.dataset.dataitems import DateTimeItem
+from guidata.dataset.qtwidgets import DataSetEditLayout
+from guidata.qthelpers import qt_app_context
+
+
+def test_datetime_display_callback() -> None:
+ """Changing the date/time calls the display callback with a datetime"""
+ calls = []
+
+ def callback(instance: DataSet, _item: DateTimeItem, value) -> None:
+ """Record the callback value and the dataset value"""
+ calls.append((value, instance.dtime))
+
+ class _DateTimeDataSet(DataSet):
+ """Dataset with a date/time item having a display callback"""
+
+ dtime = DateTimeItem(
+ "Date/time", default=datetime.datetime(2010, 10, 10, 10, 10)
+ ).set_prop("display", callback=callback)
+
+ with qt_app_context():
+ dialog = QW.QDialog()
+ dataset = _DateTimeDataSet()
+ layout = DataSetEditLayout(dialog, dataset, QW.QGridLayout(dialog))
+ try:
+ (widget,) = layout.get_terminal_widgets()
+ calls.clear()
+ widget.dateedit.setDateTime(
+ QC.QDateTime(QC.QDate(2024, 1, 2), QC.QTime(3, 4))
+ )
+ expected = datetime.datetime(2024, 1, 2, 3, 4)
+ assert calls == [(expected, expected)]
+ assert type(calls[0][0]) is datetime.datetime
+ finally:
+ dialog.deleteLater()
diff --git a/guidata/widgets/about.py b/guidata/widgets/about.py
index 7d80db4..4348a6a 100644
--- a/guidata/widgets/about.py
+++ b/guidata/widgets/about.py
@@ -114,6 +114,7 @@ def about(
Text about this package
"""
auth, year, org = self.author, self.year, self.organization
+ author, organization = auth, org
if html:
author = f"{auth}"
organization = f"{org}"