From 804843fbcc89e95044db0e80d508193b8ea2ca15 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 10:54:25 +0800 Subject: [PATCH 01/15] Port platform tests to pytest. --- core/src/toga/platform.py | 42 ++--- core/tests/test_platform.py | 305 ++++++++++++++++++++++-------------- 2 files changed, 200 insertions(+), 147 deletions(-) diff --git a/core/src/toga/platform.py b/core/src/toga/platform.py index e1cde70c25..2e02472897 100644 --- a/core/src/toga/platform.py +++ b/core/src/toga/platform.py @@ -1,7 +1,6 @@ import importlib import os import sys -import warnings from functools import lru_cache try: @@ -30,44 +29,31 @@ } -try: - current_platform = os.environ["TOGA_PLATFORM"] -except KeyError: +def get_current_platform(): # Rely on `sys.getandroidapilevel`, which only exists on Android; see # https://github.com/beeware/Python-Android-support/issues/8 if hasattr(sys, "getandroidapilevel"): - current_platform = "android" + return "android" elif sys.platform.startswith("freebsd"): - current_platform = "freeBSD" + return "freeBSD" else: - current_platform = _TOGA_PLATFORMS.get(sys.platform) + return _TOGA_PLATFORMS.get(sys.platform) + + +current_platform = get_current_platform() @lru_cache(maxsize=1) -def get_platform_factory(factory=None): - """This function figures out what the current host platform is and imports the - adequate factory. The factory is the interface to all platform specific - implementations. +def get_platform_factory(): + """Determine the current host platform and import the platform factory. - If the TOGA_BACKEND environment variable is set, the factory will be loaded + If the ``TOGA_BACKEND`` environment variable is set, the factory will be loaded from that module. - Returns: The suitable factory for the current host platform. + Raises :any:`RuntimeError` if an appropriate host platform cannot be identified. - Raises: - RuntimeError: If no supported host platform can be identified. + :returns: The factory for the host platform. """ - - ###################################################################### - # 2022-09: Backwards compatibility - ###################################################################### - # factory no longer used - if factory: - warnings.warn("The factory argument is no longer used.", DeprecationWarning) - ###################################################################### - # End backwards compatibility. - ###################################################################### - toga_backends = entry_points(group="toga.backends") if not toga_backends: raise RuntimeError("No Toga backend could be loaded.") @@ -104,7 +90,9 @@ def get_platform_factory(factory=None): ) raise RuntimeError( f"Multiple Toga backends are installed ({toga_backends_string}), " - f"but none of them match your current platform ({current_platform!r})." + f"but none of them match your current platform ({current_platform!r}). " + "Install a backend for your current platform, or use " + "TOGA_BACKEND to specify a backend." ) if len(matching_backends) > 1: toga_backends_string = ", ".join( diff --git a/core/tests/test_platform.py b/core/tests/test_platform.py index aed2d6575f..4d145e27d4 100644 --- a/core/tests/test_platform.py +++ b/core/tests/test_platform.py @@ -1,6 +1,7 @@ -import os -import unittest -from unittest.mock import Mock, patch +import sys +from unittest.mock import Mock + +import pytest import toga_dummy @@ -14,7 +15,98 @@ except ImportError: from importlib.metadata import EntryPoint -from toga.platform import current_platform, get_platform_factory +import toga.platform +from toga.platform import current_platform, get_current_platform, get_platform_factory + + +@pytest.fixture +def clean_env(monkeypatch): + monkeypatch.delenv("TOGA_BACKEND") + + +@pytest.fixture +def platform_factory_1(): + return Mock() + + +@pytest.fixture +def platform_factory_2(): + return Mock() + + +def patch_platforms(monkeypatch, platforms): + monkeypatch.setattr( + sys, + "modules", + {f"{name}_module.factory": factory for name, factory, _ in platforms}, + ) + + monkeypatch.setattr( + toga.platform, + "entry_points", + Mock( + return_value=[ + EntryPoint( + name=current_platform if is_current else name, + value=f"{name}_module", + group="self.backends", + ) + for name, _, is_current in platforms + ] + ), + ) + + +def test_get_current_platform_desktop(): + assert ( + get_current_platform() + == { + "darwin": "macOS", + "linux": "linux", + "win32": "windows", + }[sys.platform] + ) + + +def test_get_current_platform_android_inferred(monkeypatch): + "Android platform can be inferred from existence of sys.getandroidapilevel" + monkeypatch.setattr(sys, "platform", "linux") + try: + # since there isn't an existing attribute of this name, it can't be patched. + sys.getandroidapilevel = Mock(return_value=42) + assert get_current_platform() == "android" + finally: + del sys.getandroidapilevel + + +def test_get_current_platform_android(monkeypatch): + "Android platform can be obtained directly from sys.platform" + monkeypatch.setattr(sys, "platform", "android") + try: + # since there isn't an existing attribute of this name, it can't be patched. + sys.getandroidapilevel = Mock(return_value=42) + assert get_current_platform() == "android" + finally: + del sys.getandroidapilevel + + +def test_get_current_platform_iOS(monkeypatch): + "iOS platform can be obtained directly from sys.platform" + monkeypatch.setattr(sys, "platform", "ios") + assert get_current_platform() == "iOS" + + +def test_get_current_platform_web(monkeypatch): + "Web platform can be obtained directly from sys.platform" + monkeypatch.setattr(sys, "platform", "emscripten") + assert get_current_platform() == "web" + + +@pytest.mark.parametrize("value", ["freebsd12", "freebsd13", "freebsd14"]) +def test_get_current_platform_freebsd(monkeypatch, value): + "FreeBSD platform can be obtained directly from sys.platform" + monkeypatch.setattr(sys, "platform", value) + assert get_current_platform() == "freeBSD" def _get_platform_factory(): @@ -24,119 +116,92 @@ def _get_platform_factory(): return factory -@patch.dict(os.environ, {"TOGA_BACKEND": ""}) -class PlatformTests(unittest.TestCase): - def setUp(self): - super().setUp() - self.group = "toga.backends" - - def test_no_platforms(self): - with patch("toga.platform.entry_points", return_value=None): - with self.assertRaises(RuntimeError): - _get_platform_factory() - - def test_one_platform_installed(self): - only_platform_factory = Mock() - platform_factories = { - "only_platform_module.factory": only_platform_factory, - } - entry_points = [ - EntryPoint( - name="only_platform", value="only_platform_module", group=self.group - ), - ] - with patch.dict("sys.modules", platform_factories): - with patch("toga.platform.entry_points", return_value=entry_points): - factory = _get_platform_factory() - self.assertEqual(factory, only_platform_factory) - - def test_multiple_platforms_installed(self): - current_platform_factory = Mock() - other_platform_factory = Mock() - platform_factories = { - "current_platform_module.factory": current_platform_factory, - "other_platform_module.factory": other_platform_factory, - } - entry_points = [ - EntryPoint( - name=current_platform, value="current_platform_module", group=self.group - ), - EntryPoint( - name="other_platform", value="other_platform_module", group=self.group - ), - ] - with patch.dict("sys.modules", platform_factories): - with patch("toga.platform.entry_points", return_value=entry_points): - factory = _get_platform_factory() - self.assertEqual(factory, current_platform_factory) - - def test_multiple_platforms_installed_fail_both_appropriate(self): - current_platform_factory_1 = Mock() - current_platform_factory_2 = Mock() - platform_factories = { - "current_platform_module_1.factory": current_platform_factory_1, - "current_platform_module_2.factory": current_platform_factory_2, - } - entry_points = [ - EntryPoint( - name=current_platform, - value="current_platform_module_1", - group=self.group, - ), - EntryPoint( - name=current_platform, - value="current_platform_module_2", - group=self.group, - ), - ] - with patch.dict("sys.modules", platform_factories): - with patch("toga.platform.entry_points", return_value=entry_points): - with self.assertRaises(RuntimeError): - _get_platform_factory() - - def test_multiple_platforms_installed_fail_none_appropriate(self): - other_platform_factory_1 = Mock() - other_platform_factory_2 = Mock() - platform_factories = { - "other_platform_module_1.factory": other_platform_factory_1, - "other_platform_module_2.factory": other_platform_factory_2, - } - entry_points = [ - EntryPoint( - name="other_platform_1", - value="other_platform_module_1", - group=self.group, - ), - EntryPoint( - name="other_platform_2", - value="other_platform_module_2", - group=self.group, - ), - ] - with patch.dict("sys.modules", platform_factories): - with patch("toga.platform.entry_points", return_value=entry_points): - with self.assertRaises(RuntimeError): - _get_platform_factory() - - @patch.dict(os.environ, {"TOGA_BACKEND": "toga_dummy"}) - def test_environment_variable(self): - self.assertEqual(toga_dummy.factory, _get_platform_factory()) - - @patch.dict(os.environ, {"TOGA_BACKEND": "fake_platform_module"}) - def test_environment_variable_fail(self): - with self.assertRaises(RuntimeError): - _get_platform_factory() - - ###################################################################### - # 2022-09: Backwards compatibility - ###################################################################### - - def test_factory_deprecated(self): - my_factory = object() - with self.assertWarns(DeprecationWarning): - factory = get_platform_factory(factory=my_factory) - self.assertNotEqual(factory, my_factory) - - ###################################################################### - # End backwards compatibility. - ###################################################################### +def test_no_platforms(monkeypatch, clean_env): + patch_platforms(monkeypatch, []) + with pytest.raises( + RuntimeError, + match=r"No Toga backend could be loaded.", + ): + _get_platform_factory() + + +def test_one_platform_installed(monkeypatch, clean_env): + only_platform_factory = Mock() + patch_platforms(monkeypatch, [("only_platform", only_platform_factory, False)]) + + factory = _get_platform_factory() + assert factory == only_platform_factory + + +def test_multiple_platforms_installed(monkeypatch, clean_env): + current_platform_factory = Mock() + other_platform_factory = Mock() + patch_platforms( + monkeypatch, + [ + ("other_platform", other_platform_factory, False), + ("current_platform", current_platform_factory, True), + ], + ) + + factory = _get_platform_factory() + assert factory == current_platform_factory + + +def test_multiple_platforms_installed_fail_both_appropriate(monkeypatch, clean_env): + current_platform_factory_1 = Mock() + current_platform_factory_2 = Mock() + patch_platforms( + monkeypatch, + [ + ("current_platform_1", current_platform_factory_1, True), + ("current_platform_2", current_platform_factory_2, True), + ], + ) + + with pytest.raises( + RuntimeError, + match=( + r"Multiple candidate toga backends found: \('current_platform_1_module' " + r"\(.*\), 'current_platform_2_module' \(.*\)\). Uninstall the backends you " + r"don't require, or use TOGA_BACKEND to specify a backend." + ), + ): + _get_platform_factory() + + +def test_multiple_platforms_installed_fail_none_appropriate(monkeypatch, clean_env): + other_platform_factory_1 = Mock() + other_platform_factory_2 = Mock() + patch_platforms( + monkeypatch, + [ + ("other_platform_1", other_platform_factory_1, False), + ("other_platform_2", other_platform_factory_2, False), + ], + ) + + with pytest.raises( + RuntimeError, + match=( + r"Multiple Toga backends are installed \('other_platform_1_module' " + r"\(.*\), 'other_platform_2_module' \(.*\)\), but none of them match " + r"your current platform \('.*'\). Install a backend for your current " + r"platform, or use TOGA_BACKEND to specify a backend." + ), + ): + _get_platform_factory() + + +def test_environment_variable(monkeypatch): + monkeypatch.setenv("TOGA_BACKEND", "toga_dummy") + assert toga_dummy.factory == _get_platform_factory() + + +def test_environment_variable_fail(monkeypatch): + monkeypatch.setenv("TOGA_BACKEND", "fake_platform_module") + with pytest.raises( + RuntimeError, + match=r"The backend specified by TOGA_BACKEND \('fake_platform_module'\) could not be loaded.", + ): + _get_platform_factory() From 5d06ac290858680e9a0d2a7862372962481d12fa Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 10:57:35 +0800 Subject: [PATCH 02/15] Restore 100% coverage for date/time input. --- core/tests/widgets/test_dateinput.py | 6 ++++++ core/tests/widgets/test_timeinput.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/core/tests/widgets/test_dateinput.py b/core/tests/widgets/test_dateinput.py index e9e5a98bae..ae0af96e1b 100644 --- a/core/tests/widgets/test_dateinput.py +++ b/core/tests/widgets/test_dateinput.py @@ -262,3 +262,9 @@ def test_deprecated_names(): widget.max_date = MAX assert widget.max_date == MAX assert widget.max_value == MAX + + with warns(DeprecationWarning, match="DatePicker has been renamed DateInput"): + widget = toga.DatePicker() + + assert widget.min_date == date(1800, 1, 1) + assert widget.max_date == date(8999, 12, 31) diff --git a/core/tests/widgets/test_timeinput.py b/core/tests/widgets/test_timeinput.py index 70e3fa1124..0dd011b4eb 100644 --- a/core/tests/widgets/test_timeinput.py +++ b/core/tests/widgets/test_timeinput.py @@ -250,3 +250,9 @@ def test_deprecated_names(): widget.max_time = MAX assert widget.max_time == MAX assert widget.max_value == MAX + + with warns(DeprecationWarning, match="TimePicker has been renamed TimeInput"): + widget = toga.TimePicker() + + assert widget.min_time == time(0, 0, 0) + assert widget.max_time == time(23, 59, 59) From 588296b310cbe44bfe6c75e54d3b35cac661fd15 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 11:13:52 +0800 Subject: [PATCH 03/15] Restore 100% test coverage to cocoa buttons. --- cocoa/src/toga_cocoa/widgets/button.py | 3 ++- cocoa/tests_backend/widgets/button.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cocoa/src/toga_cocoa/widgets/button.py b/cocoa/src/toga_cocoa/widgets/button.py index f6dbdbf4c3..1eb11323de 100644 --- a/cocoa/src/toga_cocoa/widgets/button.py +++ b/cocoa/src/toga_cocoa/widgets/button.py @@ -2,6 +2,7 @@ from toga.colors import TRANSPARENT from toga.fonts import SYSTEM_DEFAULT_FONT_SIZE +from toga.style.pack import NONE from toga_cocoa.colors import native_color from toga_cocoa.libs import ( SEL, @@ -46,7 +47,7 @@ def _set_button_style(self): # RegularSquare button. if ( self.interface.style.font_size != SYSTEM_DEFAULT_FONT_SIZE - or self.interface.style.height + or self.interface.style.height != NONE ): self.native.bezelStyle = NSBezelStyle.RegularSquare else: diff --git a/cocoa/tests_backend/widgets/button.py b/cocoa/tests_backend/widgets/button.py index 953822d15d..2f6219df39 100644 --- a/cocoa/tests_backend/widgets/button.py +++ b/cocoa/tests_backend/widgets/button.py @@ -1,5 +1,6 @@ from pytest import xfail +from toga.style.pack import NONE from toga_cocoa.libs import NSBezelStyle, NSButton, NSFont from .base import SimpleProbe @@ -33,7 +34,7 @@ def height(self): # If the button has a manual height set, or has a non-default font size # it should have a different bezel style. if ( - self.widget.style.height + self.widget.style.height != NONE or self.native.font.pointSize != NSFont.systemFontSize ): assert self.native.bezelStyle == NSBezelStyle.RegularSquare From 5eec03b9b05ca4f866df21d6f2119c6b2e192aee Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 11:15:01 +0800 Subject: [PATCH 04/15] Add changenote. --- changes/2004.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2004.misc.rst diff --git a/changes/2004.misc.rst b/changes/2004.misc.rst new file mode 100644 index 0000000000..8e161207e2 --- /dev/null +++ b/changes/2004.misc.rst @@ -0,0 +1 @@ +Test coverage was restored for DateInput, TimeInput and Button widgets. From 240d5581997f4d6bfdfd9f1c2380c665c37f41df Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 13:57:42 +0800 Subject: [PATCH 05/15] Deprecated Slider.range in favor of Slider.min and Slider.max --- changes/1999.removal.1.rst | 1 + cocoa/src/toga_cocoa/widgets/slider.py | 15 ++- core/src/toga/widgets/slider.py | 149 +++++++++++++++++------ core/tests/widgets/test_dateinput.py | 9 +- core/tests/widgets/test_slider.py | 162 +++++++++++++++++++------ core/tests/widgets/test_timeinput.py | 9 +- dummy/src/toga_dummy/widgets/slider.py | 14 ++- gtk/src/toga_gtk/widgets/slider.py | 14 ++- iOS/src/toga_iOS/widgets/slider.py | 16 ++- testbed/tests/widgets/test_slider.py | 43 ++++--- 10 files changed, 312 insertions(+), 120 deletions(-) create mode 100644 changes/1999.removal.1.rst diff --git a/changes/1999.removal.1.rst b/changes/1999.removal.1.rst new file mode 100644 index 0000000000..8c5f27b7e7 --- /dev/null +++ b/changes/1999.removal.1.rst @@ -0,0 +1 @@ +``Slider.range`` has been replaced by ``Slider.min`` and ``Slider.max``. diff --git a/cocoa/src/toga_cocoa/widgets/slider.py b/cocoa/src/toga_cocoa/widgets/slider.py index c7deced6a2..bf80d24a4f 100644 --- a/cocoa/src/toga_cocoa/widgets/slider.py +++ b/cocoa/src/toga_cocoa/widgets/slider.py @@ -59,12 +59,17 @@ def get_value(self): def set_value(self, value): self.native.doubleValue = value - def get_range(self): - return self.native.minValue, self.native.maxValue + def get_min(self): + return self.native.minValue - def set_range(self, range): - self.native.minValue = range[0] - self.native.maxValue = range[1] + def set_min(self, value): + self.native.minValue = value + + def get_max(self): + return self.native.maxValue + + def set_max(self, value): + self.native.maxValue = value def rehint(self): content_size = self.native.intrinsicContentSize() diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 3981c8d852..d4f824b6ba 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from abc import ABC, abstractmethod from contextlib import contextmanager @@ -14,12 +15,14 @@ def __init__( id=None, style=None, value: float | None = None, - range: tuple[float, float] = (0.0, 1.0), + min: float = None, # Default to 0.0 when range is removed + max: float = None, # Default to 1.0 when range is removed tick_count: int | None = None, on_change: callable | None = None, on_press: callable | None = None, on_release: callable | None = None, enabled: bool = True, + range: tuple[float, float] = None, # DEPRECATED ): """Create a new slider widget. @@ -42,13 +45,38 @@ def __init__( super().__init__(id=id, style=style) self._impl = self.factory.Slider(interface=self) + ###################################################################### + # 2023-06: Backwards compatibility + ###################################################################### + if range is not None: + if min is not None or max is not None: + raise ValueError( + "range cannot be specifed if min and max are specified" + ) + else: + warnings.warn( + "Slider.range has been deprecated in favor of Slider.min and Slider.max", + DeprecationWarning, + ) + min, max = range + else: + # This inserts defau. + if min is None: + min = 0.0 + if max is None: + max = 1.0 + ###################################################################### + # End backwards compatibility + ###################################################################### + # Set a dummy handler before installing the actual on_change, because we do not want # on_change triggered by the initial value being set self.on_change = None - self.range = range + self.min = min + self.max = max self.tick_count = tick_count if value is None: - value = (range[0] + range[1]) / 2 + value = (min + max) / 2 self.value = value self.on_change = on_change @@ -84,11 +112,15 @@ def value(self) -> float: @value.setter def value(self, value): - if not (self.min <= value <= self.max): - raise ValueError(f"value {value} is not in range {self.min} - {self.max}") - + if value < self.min: + value = self.min + elif value > self.max: + value = self.max with self._programmatic_change(): - self._impl.set_value(self._round_value(float(value))) + self._set_value(value) + + def _set_value(self, value): + self._impl.set_value(self._round_value(float(value))) def _round_value(self, value): step = self.tick_step @@ -98,44 +130,52 @@ def _round_value(self, value): return value @property - def range(self) -> tuple[float, float]: - """Range of allowed values, in the form (min, max). - - If a range is set which doesn't include the current value, the value will be - changed to the min or the max, whichever is closest. + def min(self) -> float: + """Minimum allowed value. - :raises ValueError: If the min is not strictly less than the max. + If the new minimum value is greater than the current maximum value, + the maximum value will be increased to the new minimum value. """ - return self._impl.get_range() - - @range.setter - def range(self, range): - _min, _max = range - if _min >= _max: - raise ValueError(f"min value {_min} is not smaller than max value {_max}") + return self._impl.get_min() + @min.setter + def min(self, value): with self._programmatic_change() as old_value: # Some backends will clip the current value within the range automatically, # but do it ourselves to be certain. In discrete mode, setting self.value also # rounds to the new positions of the ticks. - self._impl.set_range((float(_min), float(_max))) - self.value = max(_min, min(_max, old_value)) - - @property - def min(self) -> float: - """Minimum allowed value. + _min = float(value) + _max = self.max + if _max < _min: + _max = _min + self._impl.set_max(_max) - This property is read-only, and depends on the value of :any:`range`. - """ - return self.range[0] + self._impl.set_min(_min) + self._set_value(max(_min, min(_max, old_value))) @property def max(self) -> float: """Maximum allowed value. - This property is read-only, and depends on the value of :any:`range`. + If the new maximum value is less than the current minimum value, + the minimum value will be decreaed to the new maximum value. """ - return self.range[1] + return self._impl.get_max() + + @max.setter + def max(self, value): + with self._programmatic_change() as old_value: + # Some backends will clip the current value within the range automatically, + # but do it ourselves to be certain. In discrete mode, setting self.value also + # rounds to the new positions of the ticks. + _min = self.min + _max = float(value) + if _min > _max: + _min = _max + self._impl.set_min(_min) + + self._impl.set_max(_max) + self._set_value(max(_min, min(_max, old_value))) @property def tick_count(self) -> int | None: @@ -181,7 +221,7 @@ def tick_step(self) -> float | None: This property is read-only, and depends on the values of :any:`tick_count` and :any:`range`. """ - if self.tick_count is None: + if self.tick_count is None or self.max == self.min: return None return (self.max - self.min) / (self.tick_count - 1) @@ -243,6 +283,23 @@ def on_release(self) -> callable: def on_release(self, handler): self._on_release = wrapped_handler(self, handler) + ###################################################################### + # 2023-06: Backwards compatibility + ###################################################################### + @property + def range(self) -> tuple[float, float]: + warnings.warn( + "Slider.range has been deprecated in favor of Slider.min and Slider.max", + DeprecationWarning, + ) + return (self.min, self.max) + + @range.setter + def range(self, range): + _min, _max = range + self.min = _min + self.max = _max + class SliderImpl(ABC): @abstractmethod @@ -254,11 +311,19 @@ def set_value(self, value): ... @abstractmethod - def get_range(self): + def get_min(self): + ... + + @abstractmethod + def set_min(self, value): ... @abstractmethod - def set_range(self, range): + def get_max(self): + ... + + @abstractmethod + def set_max(self, value): ... @abstractmethod @@ -281,6 +346,8 @@ def __init__(self): # Dummy values used during initialization. self.value = 0 + self.min = 0 + self.max = 1 self.discrete = False def get_value(self): @@ -291,11 +358,17 @@ def set_value(self, value): self.set_int_value(round((value - self.min) / span * self.get_int_max())) self.value = value # Cache the original value so we can round-trip it. - def get_range(self): - return self.min, self.max + def get_min(self): + return self.min + + def set_min(self, value): + self.min = value + + def get_max(self): + return self.max - def set_range(self, range): - self.min, self.max = range + def set_max(self, value): + self.max = value def get_tick_count(self): return (self.get_int_max() + 1) if self.discrete else None diff --git a/core/tests/widgets/test_dateinput.py b/core/tests/widgets/test_dateinput.py index ae0af96e1b..80c3ab544f 100644 --- a/core/tests/widgets/test_dateinput.py +++ b/core/tests/widgets/test_dateinput.py @@ -2,7 +2,6 @@ from unittest.mock import Mock import pytest -from pytest import warns import toga from toga_dummy.utils import assert_action_performed @@ -249,7 +248,9 @@ def test_deprecated_names(): MIN = date(2012, 8, 3) MAX = date(2016, 11, 15) - with warns(DeprecationWarning, match="DatePicker has been renamed DateInput"): + with pytest.warns( + DeprecationWarning, match="DatePicker has been renamed DateInput" + ): widget = toga.DatePicker(min_date=MIN, max_date=MAX) assert widget.min_value == MIN assert widget.max_value == MAX @@ -263,7 +264,9 @@ def test_deprecated_names(): assert widget.max_date == MAX assert widget.max_value == MAX - with warns(DeprecationWarning, match="DatePicker has been renamed DateInput"): + with pytest.warns( + DeprecationWarning, match="DatePicker has been renamed DateInput" + ): widget = toga.DatePicker() assert widget.min_date == date(1800, 1, 1) diff --git a/core/tests/widgets/test_slider.py b/core/tests/widgets/test_slider.py index f3577e9aa5..69599c29cc 100644 --- a/core/tests/widgets/test_slider.py +++ b/core/tests/widgets/test_slider.py @@ -23,7 +23,8 @@ def on_change(): def slider(on_change): return toga.Slider( value=INITIAL_VALUE, - range=(INITIAL_MIN, INITIAL_MAX), + min=INITIAL_MIN, + max=INITIAL_MAX, on_change=on_change, enabled=INITIAL_ENABLED, tick_count=INITIAL_TICK_COUNT, @@ -77,15 +78,15 @@ def test_set_value_to_be_max(slider, on_change): def test_set_value_to_be_too_small(slider, on_change): - with raises(ValueError, match="value -1 is not in range 0.0 - 100.0"): - slider.value = INITIAL_MIN - 1 - assert_value(slider, on_change, tick_value=INITIAL_TICK_VALUE, value=INITIAL_VALUE) + "Setting the value below the minimum results in clipping" + slider.value = INITIAL_MIN - 1 + assert_value(slider, on_change, tick_value=1, value=INITIAL_MIN, change_count=1) def test_set_value_to_be_too_big(slider, on_change): - with raises(ValueError, match="value 101 is not in range 0.0 - 100.0"): - slider.value = INITIAL_MAX + 1 - assert_value(slider, on_change, tick_value=INITIAL_TICK_VALUE, value=INITIAL_VALUE) + "Setting the value above the maximum results in clipping" + slider.value = INITIAL_MAX + 1 + assert_value(slider, on_change, tick_value=11, value=INITIAL_MAX, change_count=1) def test_set_tick_value_between_min_and_max(slider, on_change): @@ -112,15 +113,15 @@ def test_set_tick_value_to_be_max(slider, on_change): def test_set_tick_value_to_be_too_small(slider, on_change): - with raises(ValueError, match="value -10.0 is not in range 0.0 - 100.0"): - slider.tick_value = 0 - assert_value(slider, on_change, tick_value=INITIAL_TICK_VALUE, value=INITIAL_VALUE) + "Setting the tick value to less than the min results in clipping" + slider.tick_value = 0 + assert_value(slider, on_change, tick_value=1, value=INITIAL_MIN, change_count=1) def test_set_tick_value_to_be_too_big(slider, on_change): - with raises(ValueError, match="value 110.0 is not in range 0.0 - 100.0"): - slider.tick_value = INITIAL_TICK_COUNT + 1 - assert_value(slider, on_change, tick_value=INITIAL_TICK_VALUE, value=INITIAL_VALUE) + "Setting the tick value to greater than the max results in clipping" + slider.tick_value = INITIAL_TICK_COUNT + 1 + assert_value(slider, on_change, tick_value=11, value=INITIAL_MAX, change_count=1) def test_tick_value_without_tick_count(slider, on_change): @@ -212,16 +213,12 @@ def test_decreasing_by_ticks(slider, on_change): def test_range(slider, on_change, min, max, value): """Setting the range clamps the existing value.""" slider.tick_count = None - slider.range = (min, max) - assert isinstance(slider.range[0], float) - assert isinstance(slider.range[1], float) - assert slider.range == (min, max) - assert attribute_value(slider, "range") == (min, max) - + slider.min = min + slider.max = max assert isinstance(slider.min, float) - assert slider.min == min + assert slider.min == pytest.approx(min) assert isinstance(slider.max, float) - assert slider.max == max + assert slider.max == pytest.approx(max) assert_value( slider, @@ -231,12 +228,38 @@ def test_range(slider, on_change, min, max, value): ) -def test_invalid_range(slider, on_change): - for min, max in [(0, 0), (100, 0)]: - with raises( - ValueError, match=f"min value {min} is not smaller than max value {max}" - ): - slider.range = (min, max) +@pytest.mark.parametrize( + "new_min, new_max", + [ + [-5, 10], # less than old min + [5, 10], # more than old min, less than max + [15, 15], # more than max + ], +) +def test_min_clipping(slider, new_min, new_max): + slider.min = 0 + slider.max = 10 + + slider.min = new_min + assert slider.min == new_min + assert slider.max == new_max + + +@pytest.mark.parametrize( + "new_max, new_min", + [ + [15, 0], # less than old max + [5, 0], # less than old max, more than min + [-5, -5], # less than min + ], +) +def test_max_clipping(slider, new_max, new_min): + slider.min = 0 + slider.max = 10 + + slider.max = new_max + assert slider.min == new_min + assert slider.max == new_max def test_set_enabled_with_working_values(slider, on_change): @@ -264,7 +287,11 @@ def test_get_tick_count(slider, on_change): @pytest.mark.parametrize(TICK_PARAM_NAMES, TICK_PARAM_VALUES) def test_set_tick_count(slider, on_change, tick_count, tick_step, tick_value, value): """Setting the tick count rounds the existing value to the nearest tick.""" - slider.range = TICK_RANGE + slider.min = TICK_RANGE[0] + slider.max = TICK_RANGE[1] + # Setting min and max will send change signals. + on_change.reset_mock() + slider.tick_count = tick_count assert slider.tick_count == tick_count assert attribute_value(slider, "tick_count") == tick_count @@ -289,7 +316,11 @@ def test_set_value_with_tick_count( slider, on_change, tick_count, tick_step, tick_value, value ): """Setting the value rounds it to the nearest tick.""" - slider.range = TICK_RANGE + slider.min = TICK_RANGE[0] + slider.max = TICK_RANGE[1] + # Setting min and max will send change signals. + on_change.reset_mock() + slider.tick_count = tick_count slider.value = TICK_RANGE[1] @@ -375,8 +406,10 @@ def test_int_impl_continuous(): impl = DummyIntImpl() assert impl.get_value() == 0 - impl.set_range((0, 1)) - assert impl.get_range() == (0, 1) + impl.set_min(0) + assert impl.get_min() == 0 + impl.set_max(1) + assert impl.get_max() == 1 impl.set_tick_count(None) assert impl.get_tick_count() is None assert impl.int_max == 10000 @@ -397,8 +430,10 @@ def test_int_impl_continuous(): assert impl.get_value() == value # Check a range that doesn't start at zero. - impl.set_range((-0.4, 0.6)) - assert impl.get_range() == (-0.4, 0.6) + impl.set_min(-0.4) + assert impl.get_min() == pytest.approx(-0.4) + impl.set_max(0.6) + assert impl.get_max() == pytest.approx(0.6) impl.set_value(0.5) assert impl.get_value() == 0.5 assert impl.int_value == 9000 @@ -408,7 +443,10 @@ def test_int_impl_discrete(): impl = DummyIntImpl() assert impl.get_value() == 0 - impl.set_range((0, 1)) + impl.set_min(0) + assert impl.get_min() == 0 + impl.set_max(1) + assert impl.get_max() == 1 impl.set_tick_count(9) assert impl.get_tick_count() == 9 assert impl.int_max == 8 @@ -430,8 +468,10 @@ def test_int_impl_discrete(): assert impl.int_value == int_value # Check a range that doesn't start at zero. - impl.set_range((-0.4, 0.6)) - assert impl.get_range() == (-0.4, 0.6) + impl.set_min(-0.4) + assert impl.get_min() == pytest.approx(-0.4) + impl.set_max(0.6) + assert impl.get_max() == pytest.approx(0.6) impl.set_value(0.5) assert impl.get_value() == 0.5 assert impl.int_value == 7 @@ -453,7 +493,8 @@ def test_int_impl_discrete(): def test_int_impl_on_change(tick_count, data): """Ints should be converted into values correctly.""" impl = DummyIntImpl() - impl.set_range((0, 1)) + impl.set_min(0) + impl.set_max(1) impl.set_tick_count(tick_count) for value, int_value in data: impl.interface.reset_mock() @@ -461,3 +502,50 @@ def test_int_impl_on_change(tick_count, data): impl.on_change() assert impl.get_value() == approx(value) impl.interface.on_change.assert_called_once_with(None) + + +def test_deprecated(): + "Check the deprecated min/max naming" + # Can't specify min and range + with pytest.raises( + ValueError, + match=r"range cannot be specifed if min and max are specified", + ): + toga.Slider(min=2, range=(2, 4)) + + # Can't specify max and range + with pytest.raises( + ValueError, + match=r"range cannot be specifed if min and max are specified", + ): + toga.Slider(max=4, range=(2, 4)) + + # Can't specify min and max and range + with pytest.raises( + ValueError, + match=r"range cannot be specifed if min and max are specified", + ): + toga.Slider(min=2, max=4, range=(2, 4)) + + # Range is deprecated + with pytest.warns( + DeprecationWarning, + match="Slider.range has been deprecated in favor of Slider.min and Slider.max", + ): + widget = toga.Slider(range=(2, 4)) + + # range is converted to min/max + assert widget.min == pytest.approx(2) + assert widget.max == pytest.approx(4) + + with pytest.warns( + DeprecationWarning, + match="Slider.range has been deprecated in favor of Slider.min and Slider.max", + ): + assert widget.range == (pytest.approx(2), pytest.approx(4)) + + # range is converted to min/max + widget.range = (6, 8) + + assert widget.min == pytest.approx(6) + assert widget.max == pytest.approx(8) diff --git a/core/tests/widgets/test_timeinput.py b/core/tests/widgets/test_timeinput.py index 0dd011b4eb..8a07a71488 100644 --- a/core/tests/widgets/test_timeinput.py +++ b/core/tests/widgets/test_timeinput.py @@ -2,7 +2,6 @@ from unittest.mock import Mock import pytest -from pytest import warns import toga from toga_dummy.utils import assert_action_performed @@ -237,7 +236,9 @@ def test_deprecated_names(): MIN = time(8, 30, 59) MAX = time(10, 0, 0) - with warns(DeprecationWarning, match="TimePicker has been renamed TimeInput"): + with pytest.warns( + DeprecationWarning, match="TimePicker has been renamed TimeInput" + ): widget = toga.TimePicker(min_time=MIN, max_time=MAX) assert widget.min_value == MIN assert widget.max_value == MAX @@ -251,7 +252,9 @@ def test_deprecated_names(): assert widget.max_time == MAX assert widget.max_value == MAX - with warns(DeprecationWarning, match="TimePicker has been renamed TimeInput"): + with pytest.warns( + DeprecationWarning, match="TimePicker has been renamed TimeInput" + ): widget = toga.TimePicker() assert widget.min_time == time(0, 0, 0) diff --git a/dummy/src/toga_dummy/widgets/slider.py b/dummy/src/toga_dummy/widgets/slider.py index 45878e73f1..59236b1944 100644 --- a/dummy/src/toga_dummy/widgets/slider.py +++ b/dummy/src/toga_dummy/widgets/slider.py @@ -15,11 +15,17 @@ def get_value(self): def set_value(self, value): self._set_value("value", value) - def get_range(self): - return self._get_value("range") + def get_min(self): + return self._get_value("min", 0) - def set_range(self, range): - self._set_value("range", range) + def set_min(self, value): + self._set_value("min", value) + + def get_max(self): + return self._get_value("max", 0) + + def set_max(self, value): + self._set_value("max", value) def get_tick_count(self): return self._get_value("tick_count", None) diff --git a/gtk/src/toga_gtk/widgets/slider.py b/gtk/src/toga_gtk/widgets/slider.py index 3527ff1851..08042915b4 100644 --- a/gtk/src/toga_gtk/widgets/slider.py +++ b/gtk/src/toga_gtk/widgets/slider.py @@ -54,12 +54,18 @@ def set_value(self, value): def get_value(self): return self.native.get_value() - def set_range(self, range): - self.adj.set_lower(range[0]) + def get_min(self): + return self.adj.get_lower() + + def set_min(self, value): + self.adj.set_lower(value) self.adj.set_upper(range[1]) - def get_range(self): - return self.adj.get_lower(), self.adj.get_upper() + def get_max(self): + return self.adj.get_upper() + + def set_max(self, value): + self.adj.set_upper(value) def set_tick_count(self, tick_count): self.tick_count = tick_count diff --git a/iOS/src/toga_iOS/widgets/slider.py b/iOS/src/toga_iOS/widgets/slider.py index e124968e44..c2928087bb 100644 --- a/iOS/src/toga_iOS/widgets/slider.py +++ b/iOS/src/toga_iOS/widgets/slider.py @@ -81,13 +81,17 @@ def set_value(self, value): self.value = value self.native.setValue(value, animated=True) - def get_range(self): - return self.range + def get_min(self): + return self.native.minimumValue - def set_range(self, range): - self.native.minimumValue = range[0] - self.native.maximumValue = range[1] - self.range = range + def set_min(self, value): + self.native.minimumValue = value + + def get_max(self): + return self.native.maximumValue + + def set_max(self, value): + self.native.maximumValue = value def get_tick_count(self): return self.tick_count diff --git a/testbed/tests/widgets/test_slider.py b/testbed/tests/widgets/test_slider.py index b6274ccc28..fb7dbe9187 100644 --- a/testbed/tests/widgets/test_slider.py +++ b/testbed/tests/widgets/test_slider.py @@ -43,7 +43,8 @@ def on_change(widget): async def test_init(widget, probe): assert widget.value == 0.5 - assert widget.range == (0, 1) + assert widget.min == 0 + assert widget.max == 1 assert widget.tick_count is None assert probe.position == approx(0.5, abs=ACCURACY) @@ -60,7 +61,8 @@ async def test_init_handlers(): # Bounds checks are covered by core tests. async def test_value(widget, probe, on_change): for scale in SCALES: - widget.range = (0, scale) + widget.min = 0 + widget.max = scale for position in POSITIONS: on_change.reset_mock() assert_set_value(widget, position * scale) @@ -78,13 +80,14 @@ async def test_value(widget, probe, on_change): def assert_set_value(widget, value_in, value_out=None): if value_out is None: value_out = value_in - value_out = assert_set_get(widget, "value", value_in, value_out) + value_out = assert_set_get(widget, "value", value_in, pytest.approx(value_out)) assert isinstance(value_out, float) async def test_change(widget, probe, on_change): for scale in SCALES: - widget.range = (0, scale) + widget.min = 0 + widget.max = scale for position in POSITIONS: on_change.reset_mock() probe.change(position) @@ -103,16 +106,17 @@ async def test_change(widget, probe, on_change): async def test_min(widget, probe, on_change): for min in POSITIONS[:-1]: on_change.reset_mock() - assert_set_range(widget, min, 1) + min_out = assert_set_get(widget, "min", min, expected=pytest.approx(min)) + assert isinstance(min_out, float) if min <= 0.5: # The existing value is in the range, so it should not change. - assert widget.value == 0.5 + assert widget.value == pytest.approx(0.5) assert probe.position == approx((0.5 - min) / (1 - min), abs=ACCURACY) on_change.assert_not_called() else: # The existing value is out of the range, so it should be clipped. - assert widget.value == min + assert widget.value == pytest.approx(min) assert probe.position == 0 on_change.assert_called_once_with(widget) await probe.redraw("Slider min property should be %s" % min) @@ -123,27 +127,22 @@ async def test_max(widget, probe, on_change): # If the existing value is in the range, it should not change. for max in POSITIONS[-1:0:-1]: on_change.reset_mock() - assert_set_range(widget, 0, max) + max_out = assert_set_get(widget, "max", max, expected=pytest.approx(max)) + assert isinstance(max_out, float) if max >= 0.5: # The existing value is in the range, so it should not change. - assert widget.value == 0.5 + assert widget.value == pytest.approx(0.5) assert probe.position == approx(0.5 / max, abs=ACCURACY) on_change.assert_not_called() else: # The existing value is out of the range, so it should be clipped. - assert widget.value == max + assert widget.value == pytest.approx(max) assert probe.position == 1 on_change.assert_called_once_with(widget) await probe.redraw("Slider max property should be %s" % max) -def assert_set_range(widget, min_in, max_in): - min_out, max_out = assert_set_get(widget, "range", (min_in, max_in)) - assert isinstance(min_out, float) - assert isinstance(max_out, float) - - # Bounds checks and all other tick functionality are covered by the core tests. async def test_ticks(widget, probe, on_change): widget.value = prev_value = 0.6 @@ -175,7 +174,8 @@ async def test_ticks(widget, probe, on_change): async def test_value_with_ticks(widget, probe, on_change): widget.tick_count = 5 - widget.range = (0, 10) + widget.min = 0 + widget.max = 10 widget.value = prev_value = 5 for value_in, value_out in [ @@ -205,18 +205,21 @@ async def test_value_with_ticks(widget, probe, on_change): async def test_range_with_ticks(widget, probe, on_change): widget.tick_count = 5 - widget.range = (0, 10) + widget.min = 0 + widget.max = 10 widget.value = prev_value = 5 for min, max, value in [ (0, 9, 4.5), (0, 10, 5), + (1, 10, 5.5), (1, 9, 5), (1, 10, 5.5), ]: on_change.reset_mock() - widget.range = (min, max) - assert widget.value == value + widget.min = min + widget.max = max + assert widget.value == pytest.approx(value) assert probe.position == approx((value - min) / (max - min), abs=ACCURACY) if value == prev_value: From 003687ba50c0d78ccb135e4ee877412d8537c410 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 14:18:26 +0800 Subject: [PATCH 06/15] Rename NumberInput.min_value/max_value --- changes/1999.removal.2.rst | 1 + core/src/toga/widgets/numberinput.py | 151 +++++++++++++------ core/tests/widgets/test_numberinput.py | 159 ++++++++++++++------ dummy/src/toga_dummy/widgets/numberinput.py | 4 +- testbed/tests/widgets/test_numberinput.py | 4 +- 5 files changed, 219 insertions(+), 100 deletions(-) create mode 100644 changes/1999.removal.2.rst diff --git a/changes/1999.removal.2.rst b/changes/1999.removal.2.rst new file mode 100644 index 0000000000..3b102fe4ff --- /dev/null +++ b/changes/1999.removal.2.rst @@ -0,0 +1 @@ +``NumberInput.min_value`` and ``NumberInput.max_value`` have been renamed ``NumberInput.min`` and ``NumberInput.max``, respectively. diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 6db57d10c9..b770aa6802 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +import warnings from decimal import ROUND_HALF_UP, Decimal, InvalidOperation from toga.handlers import wrapped_handler @@ -10,7 +11,7 @@ # Implementation notes # ==================== # -# * `step`, `min_value` and `max_value` maintain an interface shadow copy of +# * `step`, `min` and `max` maintain an interface shadow copy of # their current values. This is because we use Decimal as a representation, # but all the implementations use floats. To ensure that we can round-trip # step/min/max values, we need to keep a local copy. @@ -66,11 +67,13 @@ def __init__( id=None, style=None, step: Decimal = 1, - min_value: Decimal | None = None, - max_value: Decimal | None = None, + min: Decimal | None = None, + max: Decimal | None = None, value: Decimal | None = None, readonly: bool = False, on_change: callable | None = None, + min_value: Decimal | None = None, # DEPRECATED + max_value: Decimal | None = None, # DEPRECATED ): """Create a new number input widget. @@ -81,31 +84,55 @@ def __init__( will be applied to the widget. :param step: The amount that any increment/decrement operations will apply to the widget's current value. - :param min_value: If provided, ``value`` will be guaranteed to + :param min: If provided, ``value`` will be guaranteed to be greater than or equal to this minimum. - :param max_value: If provided, ``value`` will be guaranteed to + :param max: If provided, ``value`` will be guaranteed to be less than or equal to this maximum. :param value: The initial value for the widget. :param readonly: Can the value of the widget be modified by the user? :param on_change: A handler that will be invoked when the the value of the widget changes. """ - super().__init__(id=id, style=style) - # The initial setting of min/min_value requires calling get_value(), - # which in turn interrogates min/max_value. Prime those values with + ###################################################################### + # 2023-06: Backwards compatibility + ###################################################################### + if min_value is not None: + if min is not None: + raise ValueError("Cannot specify both min and min_value") + else: + warnings.warn( + "NumberInput.min_value has been renamed NumberInput.min", + DeprecationWarning, + ) + min = min_value + if max_value is not None: + if max is not None: + raise ValueError("Cannot specify both max and max_value") + else: + warnings.warn( + "NumberInput.max_value has been renamed NumberInput.max", + DeprecationWarning, + ) + max = max_value + ###################################################################### + # End backwards compatibility + ###################################################################### + + # The initial setting of min requires calling get_value(), + # which in turn interrogates min. Prime those values with # an empty starting value - self._min_value = None - self._max_value = None + self._min = None + self._max = None self.on_change = None self._impl = self.factory.NumberInput(interface=self) self.readonly = readonly self.step = step - self.min_value = min_value - self.max_value = max_value + self.min = min + self.max = max self.value = value self.on_change = on_change @@ -142,22 +169,22 @@ def step(self, step): self._impl.set_step(self._step) # Re-assigning the min and max value forces the min/max to be requantized. - self.min_value = self.min_value - self.max_value = self.max_value + self.min = self.min + self.max = self.max @property - def min_value(self) -> Decimal | None: + def min(self) -> Decimal | None: """The minimum bound for the widget's value. Returns ``None`` if there is no minimum bound. - If the current ``value`` is less than a newly specified ``min_value``, + If the current ``value`` is less than a newly specified ``min``, ``value`` will be clipped to conform to the new minimum. """ - return self._min_value + return self._min - @min_value.setter - def min_value(self, new_min): + @min.setter + def min(self, new_min): try: new_min = _clean_decimal(new_min, self.step) @@ -168,33 +195,29 @@ def min_value(self, new_min): if new_min is None or new_min == "": new_min = None else: - raise ValueError("min_value must be a number or None") + raise ValueError("min must be a number or None") - if ( - self.max_value is not None - and new_min is not None - and new_min > self.max_value - ): + if self.max is not None and new_min is not None and new_min > self.max: raise ValueError( - f"min value of {new_min} is greater than the current max_value of {self.max_value}" + f"min value of {new_min} is greater than the current max of {self.max}" ) - self._min_value = new_min + self._min = new_min self._impl.set_min_value(new_min) @property - def max_value(self) -> Decimal | None: + def max(self) -> Decimal | None: """The maximum bound for the widget's value. Returns ``None`` if there is no maximum bound. - If the current ``value`` exceeds a newly specified ``max_value``, + If the current ``value`` exceeds a newly specified ``max``, ``value`` will be clipped to conform to the new maximum. """ - return self._max_value + return self._max - @max_value.setter - def max_value(self, new_max): + @max.setter + def max(self, new_max): try: new_max = _clean_decimal(new_max, self.step) @@ -205,18 +228,14 @@ def max_value(self, new_max): if new_max is None or new_max == "": new_max = None else: - raise ValueError("max_value must be a number or None") + raise ValueError("max must be a number or None") - if ( - self.min_value is not None - and new_max is not None - and new_max < self.min_value - ): + if self.min is not None and new_max is not None and new_max < self.min: raise ValueError( - f"max value of {new_max} is less than the current min_value of {self.min_value}" + f"max value of {new_max} is less than the current min of {self.min}" ) - self._max_value = new_max + self._max = new_max self._impl.set_max_value(new_max) @property @@ -239,10 +258,10 @@ def value(self) -> Decimal | None: # If the widget has a current value, clip it if value is not None: - if self.min_value is not None and value < self.min_value: - return self.min_value - elif self.max_value is not None and value > self.max_value: - return self.max_value + if self.min is not None and value < self.min: + return self.min + elif self.max is not None and value > self.max: + return self.max return value @value.setter @@ -250,10 +269,10 @@ def value(self, value): try: value = _clean_decimal(value, self.step) - if self.min_value is not None and value < self.min_value: - value = self.min_value - elif self.max_value is not None and value > self.max_value: - value = self.max_value + if self.min is not None and value < self.min: + value = self.min + elif self.max is not None and value > self.max: + value = self.max except (TypeError, ValueError, InvalidOperation): if value is None or value == "": value = None @@ -271,3 +290,39 @@ def on_change(self) -> callable: @on_change.setter def on_change(self, handler): self._on_change = wrapped_handler(self, handler) + + ###################################################################### + # 2023-06: Backwards compatibility + ###################################################################### + + @property + def min_value(self) -> Decimal | None: + warnings.warn( + "NumberInput.min_value has been renamed NumberInput.min", + DeprecationWarning, + ) + return self.min + + @min_value.setter + def min_value(self, value): + warnings.warn( + "NumberInput.min_value has been renamed NumberInput.min", + DeprecationWarning, + ) + self.min = value + + @property + def max_value(self) -> Decimal | None: + warnings.warn( + "NumberInput.max_value has been renamed NumberInput.max", + DeprecationWarning, + ) + return self.max + + @max_value.setter + def max_value(self, value): + warnings.warn( + "NumberInput.max_value has been renamed NumberInput.max", + DeprecationWarning, + ) + self.max = value diff --git a/core/tests/widgets/test_numberinput.py b/core/tests/widgets/test_numberinput.py index e3d77d31bf..11d00b45cf 100644 --- a/core/tests/widgets/test_numberinput.py +++ b/core/tests/widgets/test_numberinput.py @@ -27,8 +27,8 @@ def test_widget_created(): assert not widget.readonly assert widget.value is None assert widget.step == Decimal("1") - assert widget.min_value is None - assert widget.max_value is None + assert widget.min is None + assert widget.max is None assert widget._on_change._raw is None @@ -39,8 +39,8 @@ def test_create_with_values(): widget = toga.NumberInput( value=Decimal("2.71828"), step=0.001, - min_value=-42, - max_value=420, + min=-42, + max=420, readonly=True, on_change=on_change, ) @@ -50,8 +50,8 @@ def test_create_with_values(): assert widget.readonly assert widget.value == Decimal("2.718") assert widget.step == Decimal("0.001") - assert widget.min_value == Decimal("-42") - assert widget.max_value == Decimal("420") + assert widget.min == Decimal("-42") + assert widget.max == Decimal("420") assert widget._on_change._raw == on_change # Change handler hasn't been invoked @@ -216,13 +216,13 @@ def test_bad_step(widget, value): (None, None), ], ) -def test_min_value(widget, value, expected): - "The min_value of the widget can be set" - widget.min_value = value - assert widget.min_value == expected +def test_min(widget, value, expected): + "The min of the widget can be set" + widget.min = value + assert widget.min == expected # test backend has the right value - assert attribute_value(widget, "min_value") == expected + assert attribute_value(widget, "min") == expected @pytest.mark.parametrize( @@ -232,34 +232,34 @@ def test_min_value(widget, value, expected): "Not a number", # Non-numerical string ], ) -def test_bad_min_value(widget, value): - "If a min_value can't be converted into a decimal, an error is raised" - with pytest.raises(ValueError, match=r"min_value must be a number or None"): - widget.min_value = value +def test_bad_min(widget, value): + "If a min can't be converted into a decimal, an error is raised" + with pytest.raises(ValueError, match=r"min must be a number or None"): + widget.min = value def test_min_greater_than_max(widget): "If the new min value exceeds the max value, an error is raised" - widget.max_value = 10 + widget.max = 10 with pytest.raises( ValueError, - match=r"min value of 100.00 is greater than the current max_value of 10.00", + match=r"min value of 100.00 is greater than the current max of 10.00", ): - widget.min_value = 100 + widget.min = 100 @pytest.mark.parametrize(*QUANTIZE_PARAMS) -def test_min_value_quantized(widget, step, expected): +def test_min_quantized(widget, step, expected): "An existing min value is re-quantized after a change in step" # Set a small step so that the min value isn't quantized widget.step = 0.00000001 - widget.min_value = 12.3456 + widget.min = 12.3456 # Set a new minimum widget.step = step # The minimum has been re-quantized - assert widget.min_value == expected + assert widget.min == expected @pytest.mark.parametrize( @@ -286,13 +286,13 @@ def test_min_value_quantized(widget, step, expected): (None, None), ], ) -def test_max_value(widget, value, expected): - "The max_value of the widget can be set" - widget.max_value = value - assert widget.max_value == expected +def test_max(widget, value, expected): + "The max of the widget can be set" + widget.max = value + assert widget.max == expected # test backend has the right value - assert attribute_value(widget, "max_value") == expected + assert attribute_value(widget, "max") == expected @pytest.mark.parametrize( @@ -302,38 +302,38 @@ def test_max_value(widget, value, expected): "Not a number", # Non-numerical string ], ) -def test_bad_max_value(widget, value): - "If a max_value can't be converted into a decimal, an error is raised" - with pytest.raises(ValueError, match=r"max_value must be a number or None"): - widget.max_value = value +def test_bad_max(widget, value): + "If a max can't be converted into a decimal, an error is raised" + with pytest.raises(ValueError, match=r"max must be a number or None"): + widget.max = value def test_max_less_than_min(widget): "If the new max value is less than the min value, an error is raised" - widget.min_value = 100 + widget.min = 100 with pytest.raises( ValueError, - match=r"max value of 10.00 is less than the current min_value of 100.00", + match=r"max value of 10.00 is less than the current min of 100.00", ): - widget.max_value = 10 + widget.max = 10 @pytest.mark.parametrize(*QUANTIZE_PARAMS) -def test_max_value_quantized(widget, step, expected): +def test_max_quantized(widget, step, expected): "An existing max value is re-quantized after a change in step" # Set a small step so that the max value isn't quantized widget.step = 0.00000001 - widget.max_value = 12.3456 + widget.max = 12.3456 # Set a new maximum widget.step = step # The maximum has been re-quantized - assert widget.max_value == expected + assert widget.max == expected @pytest.mark.parametrize( - "min_value, max_value, provided, clipped", + "min, max, provided, clipped", [ (10, 20, 15, Decimal(15)), (10, 20, 25, Decimal(20)), @@ -344,17 +344,17 @@ def test_max_value_quantized(widget, step, expected): (-10, 0, 25, Decimal(0)), ], ) -def test_clip_on_value_change(widget, min_value, max_value, provided, clipped): +def test_clip_on_value_change(widget, min, max, provided, clipped): "A widget's value will be clipped inside the min/max range." - widget.min_value = min_value - widget.max_value = max_value + widget.min = min + widget.max = max widget.value = provided assert widget.value == clipped @pytest.mark.parametrize( - "min_value, max_value, provided, clipped", + "min, max, provided, clipped", [ (10, 20, 15, Decimal(15)), (10, 20, 25, Decimal(20)), @@ -368,10 +368,10 @@ def test_clip_on_value_change(widget, min_value, max_value, provided, clipped): (-20, -10, 0, Decimal(-10)), ], ) -def test_clip_on_retrieval(widget, min_value, max_value, provided, clipped): +def test_clip_on_retrieval(widget, min, max, provided, clipped): "A widget's value will be clipped if the widget has a value outside the min/max range." - widget.min_value = min_value - widget.max_value = max_value + widget.min = min + widget.max = max # Inject a raw attribute value. widget._impl._set_value("value", provided) @@ -391,11 +391,11 @@ def test_clip_on_retrieval(widget, min_value, max_value, provided, clipped): def test_clip_on_max_change(widget, value, new_max, clipped): "A widget's value will be clipped if the max value changes" # Set an initial max, and a value that is less than it. - widget.max_value = 20 + widget.max = 20 widget.value = value # Set a new max - widget.max_value = new_max + widget.max = new_max # Value might be clipped assert widget.value == clipped @@ -413,11 +413,11 @@ def test_clip_on_max_change(widget, value, new_max, clipped): def test_clip_on_min_change(widget, value, new_min, clipped): "A widget's value will be clipped if the min value changes" # Set an initial max, and a value that is less than it. - widget.min_value = 10 + widget.min = 10 widget.value = value # Set a new max - widget.min_value = new_min + widget.min = new_min # Value might be clipped assert widget.value == clipped @@ -496,3 +496,66 @@ def test_clean_decimal_str(value, clean): ) def test_clean_decimal(value, step, clean): assert _clean_decimal(value, Decimal(step) if step else step) == Decimal(clean) + + +def test_deprecated_names(): + """The deprecated min_value/max_value names still work""" + # Can't specify min and min_value + with pytest.raises( + ValueError, + match=r"Cannot specify both min and min_value", + ): + toga.NumberInput(min=2, min_value=4) + + # Can't specify min and min_value + with pytest.raises( + ValueError, + match=r"Cannot specify both max and max_value", + ): + toga.NumberInput(max=2, max_value=4) + + # min_value is deprecated + with pytest.warns( + DeprecationWarning, + match="NumberInput.min_value has been renamed NumberInput.min", + ): + widget = toga.NumberInput(min_value=2) + + assert widget.min == 2 + + with pytest.warns( + DeprecationWarning, + match="NumberInput.min_value has been renamed NumberInput.min", + ): + assert widget.min_value == 2 + + with pytest.warns( + DeprecationWarning, + match="NumberInput.min_value has been renamed NumberInput.min", + ): + widget.min_value = 4 + + assert widget.min == 4 + + # max_value is deprecated + with pytest.warns( + DeprecationWarning, + match="NumberInput.max_value has been renamed NumberInput.max", + ): + widget = toga.NumberInput(max_value=2) + + assert widget.max == 2 + + with pytest.warns( + DeprecationWarning, + match="NumberInput.max_value has been renamed NumberInput.max", + ): + assert widget.max_value == 2 + + with pytest.warns( + DeprecationWarning, + match="NumberInput.max_value has been renamed NumberInput.max", + ): + widget.max_value = 4 + + assert widget.max == 4 diff --git a/dummy/src/toga_dummy/widgets/numberinput.py b/dummy/src/toga_dummy/widgets/numberinput.py index 01b0addb79..024a273fdb 100644 --- a/dummy/src/toga_dummy/widgets/numberinput.py +++ b/dummy/src/toga_dummy/widgets/numberinput.py @@ -19,10 +19,10 @@ def set_step(self, step): self._set_value("step", step) def set_min_value(self, value): - self._set_value("min_value", value) + self._set_value("min", value) def set_max_value(self, value): - self._set_value("max_value", value) + self._set_value("max", value) def set_value(self, value): self._set_value("value", value) diff --git a/testbed/tests/widgets/test_numberinput.py b/testbed/tests/widgets/test_numberinput.py index d1ef0e7c31..9873f49d19 100644 --- a/testbed/tests/widgets/test_numberinput.py +++ b/testbed/tests/widgets/test_numberinput.py @@ -102,8 +102,8 @@ async def test_on_change_handler(widget, probe): async def test_focus_value_clipping(widget, probe, other): "Widget value is clipped to min/max values when focus is lost." # Set min/max values, and a granular step - widget.min_value = Decimal(100) - widget.max_value = Decimal(2000) + widget.min = Decimal(100) + widget.max = Decimal(2000) widget.step = 1 # Install a handler, and give the widget focus. From bec1b38883f5edd039ac15c25751dcdd5b61dc90 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 14:34:40 +0800 Subject: [PATCH 07/15] Rename Date/Time min/max values. --- changes/1951.removal.2.rst | 2 +- changes/1951.removal.3.rst | 1 + changes/1951.removal.4.rst | 1 + core/src/toga/widgets/dateinput.py | 92 ++++++++++++--------- core/src/toga/widgets/timeinput.py | 92 ++++++++++++--------- core/tests/widgets/test_dateinput.py | 115 +++++++++++++++------------ core/tests/widgets/test_timeinput.py | 115 +++++++++++++++------------ 7 files changed, 241 insertions(+), 177 deletions(-) create mode 100644 changes/1951.removal.3.rst create mode 100644 changes/1951.removal.4.rst diff --git a/changes/1951.removal.2.rst b/changes/1951.removal.2.rst index 697d6e1f85..8e47bd7180 100644 --- a/changes/1951.removal.2.rst +++ b/changes/1951.removal.2.rst @@ -1 +1 @@ -``TimePicker`` has been renamed ``TimeInput``. +``DatePicker.min_date`` and ``DatePicker.max_date`` has been renamed ``DateInput.min`` and ``DateInput.max``, respectively. diff --git a/changes/1951.removal.3.rst b/changes/1951.removal.3.rst new file mode 100644 index 0000000000..697d6e1f85 --- /dev/null +++ b/changes/1951.removal.3.rst @@ -0,0 +1 @@ +``TimePicker`` has been renamed ``TimeInput``. diff --git a/changes/1951.removal.4.rst b/changes/1951.removal.4.rst new file mode 100644 index 0000000000..3b2264aa6b --- /dev/null +++ b/changes/1951.removal.4.rst @@ -0,0 +1 @@ +``TimePicker.min_time`` and ``TimePicker.max_time`` has been renamed ``TimeInput.min`` and ``TimeInput.max``, respectively. diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 0014ed9571..f4b5130ade 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -23,8 +23,8 @@ def __init__( id=None, style=None, value: datetime.date | None = None, - min_value: datetime.date | None = None, - max_value: datetime.date | None = None, + min: datetime.date | None = None, + max: datetime.date | None = None, on_change: callable | None = None, ): """Create a new DateInput widget. @@ -36,8 +36,8 @@ def __init__( will be applied to the widget. :param value: The initial date to display. If not specified, the current date will be used. - :param min_value: The earliest date (inclusive) that can be selected. - :param max_value: The latest date (inclusive) that can be selected. + :param min: The earliest date (inclusive) that can be selected. + :param max: The latest date (inclusive) that can be selected. :param on_change: A handler that will be invoked when the value changes. """ super().__init__(id=id, style=style) @@ -46,8 +46,8 @@ def __init__( self._impl = self.factory.DateInput(interface=self) self.on_change = None - self.min_value = min_value - self.max_value = max_value + self.min = min + self.max = max self.value = value self.on_change = on_change @@ -88,60 +88,60 @@ def _convert_date(self, value, *, check_range): def value(self, value): value = self._convert_date(value, check_range=False) - if value < self.min_value: - value = self.min_value - elif value > self.max_value: - value = self.max_value + if value < self.min: + value = self.min + elif value > self.max: + value = self.max self._impl.set_value(value) @property - def min_value(self) -> datetime.date: + def min(self) -> datetime.date: """The minimum allowable date (inclusive). A value of ``None`` will be converted into the lowest supported date of 1800-01-01. - The existing ``value`` and ``max_value`` will be clipped to the new minimum. + The existing ``value`` and ``max`` will be clipped to the new minimum. :raises ValueError: If set to a date outside of the supported range. """ return self._impl.get_min_date() - @min_value.setter - def min_value(self, value): + @min.setter + def min(self, value): if value is None: - min_value = MIN_DATE + min = MIN_DATE else: - min_value = self._convert_date(value, check_range=True) + min = self._convert_date(value, check_range=True) - if self.max_value < min_value: - self._impl.set_max_date(min_value) - self._impl.set_min_date(min_value) - if self.value < min_value: - self.value = min_value + if self.max < min: + self._impl.set_max_date(min) + self._impl.set_min_date(min) + if self.value < min: + self.value = min @property - def max_value(self) -> datetime.date: + def max(self) -> datetime.date: """The maximum allowable date (inclusive). A value of ``None`` will be converted into the highest supported date of 8999-12-31. - The existing ``value`` and ``min_value`` will be clipped to the new maximum. + The existing ``value`` and ``min`` will be clipped to the new maximum. :raises ValueError: If set to a date outside of the supported range. """ return self._impl.get_max_date() - @max_value.setter - def max_value(self, value): + @max.setter + def max(self, value): if value is None: - max_value = MAX_DATE + max = MAX_DATE else: - max_value = self._convert_date(value, check_range=True) + max = self._convert_date(value, check_range=True) - if self.min_value > max_value: - self._impl.set_min_date(max_value) - self._impl.set_max_date(max_value) - if self.value > max_value: - self.value = max_value + if self.min > max: + self._impl.set_min_date(max) + self._impl.set_max_date(max) + if self.value > max: + self.value = max @property def on_change(self) -> callable: @@ -159,11 +159,15 @@ def __init__(self, *args, **kwargs): warnings.warn("DatePicker has been renamed DateInput.", DeprecationWarning) for old_name, new_name in [ - ("min_date", "min_value"), - ("max_date", "max_value"), + ("min_date", "min"), + ("max_date", "max"), ]: try: value = kwargs.pop(old_name) + warnings.warn( + f"DatePicker.{old_name} has been renamed DateInput.{new_name}", + DeprecationWarning, + ) except KeyError: pass else: @@ -173,16 +177,28 @@ def __init__(self, *args, **kwargs): @property def min_date(self): - return self.min_value + warnings.warn( + "DatePicker.min_date has been renamed DateInput.min", DeprecationWarning + ) + return self.min @min_date.setter def min_date(self, value): - self.min_value = value + warnings.warn( + "DatePicker.min_date has been renamed DateInput.min", DeprecationWarning + ) + self.min = value @property def max_date(self): - return self.max_value + warnings.warn( + "DatePicker.max_date has been renamed DateInput.max", DeprecationWarning + ) + return self.max @max_date.setter def max_date(self, value): - self.max_value = value + warnings.warn( + "DatePicker.max_date has been renamed DateInput.max", DeprecationWarning + ) + self.max = value diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index b8c3b0d5b7..8a0342eac8 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -14,8 +14,8 @@ def __init__( id=None, style=None, value: datetime.time | None = None, - min_value: datetime.time | None = None, - max_value: datetime.time | None = None, + min: datetime.time | None = None, + max: datetime.time | None = None, on_change: callable | None = None, ): """Create a new TimeInput widget. @@ -27,8 +27,8 @@ def __init__( will be applied to the widget. :param value: The initial time to display. If not specified, the current time will be used. - :param min_value: The earliest time (inclusive) that can be selected. - :param max_value: The latest time (inclusive) that can be selected. + :param min: The earliest time (inclusive) that can be selected. + :param max: The latest time (inclusive) that can be selected. :param on_change: A handler that will be invoked when the value changes. """ super().__init__(id=id, style=style) @@ -37,8 +37,8 @@ def __init__( self._impl = self.factory.TimeInput(interface=self) self.on_change = None - self.min_value = min_value - self.max_value = max_value + self.min = min + self.max = max self.value = value self.on_change = on_change @@ -71,56 +71,56 @@ def _convert_time(self, value): def value(self, value): value = self._convert_time(value) - if value < self.min_value: - value = self.min_value - elif value > self.max_value: - value = self.max_value + if value < self.min: + value = self.min + elif value > self.max: + value = self.max self._impl.set_value(value) @property - def min_value(self) -> datetime.time: + def min(self) -> datetime.time: """The minimum allowable time (inclusive). A value of ``None`` will be converted into 00:00:00. - The existing ``value`` and ``max_value`` will be clipped to the new minimum. + The existing ``value`` and ``max`` will be clipped to the new minimum. """ return self._impl.get_min_time() - @min_value.setter - def min_value(self, value): + @min.setter + def min(self, value): if value is None: - min_value = datetime.time(0, 0, 0) + min = datetime.time(0, 0, 0) else: - min_value = self._convert_time(value) + min = self._convert_time(value) - if self.max_value < min_value: - self._impl.set_max_time(min_value) - self._impl.set_min_time(min_value) - if self.value < min_value: - self.value = min_value + if self.max < min: + self._impl.set_max_time(min) + self._impl.set_min_time(min) + if self.value < min: + self.value = min @property - def max_value(self) -> datetime.time: + def max(self) -> datetime.time: """The maximum allowable time (inclusive). A value of ``None`` will be converted into 23:59:59. - The existing ``value`` and ``min_value`` will be clipped to the new maximum. + The existing ``value`` and ``min`` will be clipped to the new maximum. """ return self._impl.get_max_time() - @max_value.setter - def max_value(self, value): + @max.setter + def max(self, value): if value is None: - max_value = datetime.time(23, 59, 59) + max = datetime.time(23, 59, 59) else: - max_value = self._convert_time(value) + max = self._convert_time(value) - if self.min_value > max_value: - self._impl.set_min_time(max_value) - self._impl.set_max_time(max_value) - if self.value > max_value: - self.value = max_value + if self.min > max: + self._impl.set_min_time(max) + self._impl.set_max_time(max) + if self.value > max: + self.value = max @property def on_change(self) -> callable: @@ -138,10 +138,14 @@ def __init__(self, *args, **kwargs): warnings.warn("TimePicker has been renamed TimeInput", DeprecationWarning) for old_name, new_name in [ - ("min_time", "min_value"), - ("max_time", "max_value"), + ("min_time", "min"), + ("max_time", "max"), ]: try: + warnings.warn( + f"TimePicker.{old_name} has been renamed TimeInput.{new_name}", + DeprecationWarning, + ) value = kwargs.pop(old_name) except KeyError: pass @@ -152,16 +156,28 @@ def __init__(self, *args, **kwargs): @property def min_time(self): - return self.min_value + warnings.warn( + "TimePicker.min_time has been renamed TimeInput.min", DeprecationWarning + ) + return self.min @min_time.setter def min_time(self, value): - self.min_value = value + warnings.warn( + "TimePicker.min_time has been renamed TimeInput.min", DeprecationWarning + ) + self.min = value @property def max_time(self): - return self.max_value + warnings.warn( + "TimePicker.max_time has been renamed TimeInput.max", DeprecationWarning + ) + return self.max @max_time.setter def max_time(self, value): - self.max_value = value + warnings.warn( + "TimePicker.max_time has been renamed TimeInput.max", DeprecationWarning + ) + self.max = value diff --git a/core/tests/widgets/test_dateinput.py b/core/tests/widgets/test_dateinput.py index 80c3ab544f..fb7181f0f7 100644 --- a/core/tests/widgets/test_dateinput.py +++ b/core/tests/widgets/test_dateinput.py @@ -27,8 +27,8 @@ def test_widget_created(): assert_action_performed(widget, "create DateInput") assert widget.value == date(2023, 5, 25) - assert widget.min_value == date(1800, 1, 1) - assert widget.max_value == date(8999, 12, 31) + assert widget.min == date(1800, 1, 1) + assert widget.max == date(8999, 12, 31) assert widget.on_change._raw is None @@ -37,16 +37,16 @@ def test_widget_created_with_values(on_change_handler): # Round trip the impl/interface widget = toga.DateInput( value=date(2015, 6, 15), - min_value=date(2013, 5, 14), - max_value=date(2017, 7, 16), + min=date(2013, 5, 14), + max=date(2017, 7, 16), on_change=on_change_handler, ) assert widget._impl.interface == widget assert_action_performed(widget, "create DateInput") assert widget.value == date(2015, 6, 15) - assert widget.min_value == date(2013, 5, 14) - assert widget.max_value == date(2017, 7, 16) + assert widget.min == date(2013, 5, 14) + assert widget.max == date(2017, 7, 16) assert widget.on_change._raw == on_change_handler # The change handler isn't invoked at construction. @@ -106,8 +106,8 @@ def test_invalid_value(widget, value, exc, message): def test_value_clipping(widget, value, clipped, on_change_handler): "It the value is inconsistent with min/max, it is clipped." # Set min/max dates, and clear the on_change mock - widget.min_value = date(2010, 1, 1) - widget.max_value = date(2020, 1, 1) + widget.min = date(2010, 1, 1) + widget.max = date(2020, 1, 1) on_change_handler.reset_mock() # Set the new value @@ -129,11 +129,11 @@ def test_value_clipping(widget, value, clipped, on_change_handler): ("2023-03-11", date(2023, 3, 11)), ], ) -def test_min_value(widget, value, expected): - "The min_value of the datepicker can be set" - widget.min_value = value +def test_min(widget, value, expected): + "The min of the datepicker can be set" + widget.min = value - assert widget.min_value == expected + assert widget.min == expected INVALID_LIMITS = INVALID_VALUES + [ @@ -143,16 +143,16 @@ def test_min_value(widget, value, expected): @pytest.mark.parametrize("value, exc, message", INVALID_LIMITS) -def test_invalid_min_value(widget, value, exc, message): - "Invalid min_value values raise an exception" - widget.max_value = date(2025, 6, 12) +def test_invalid_min(widget, value, exc, message): + "Invalid min values raise an exception" + widget.max = date(2025, 6, 12) with pytest.raises(exc, match=message): - widget.min_value = value + widget.min = value @pytest.mark.parametrize( - "min_value, clip_value, clip_max", + "min, clip_value, clip_max", [ (date(2005, 6, 1), False, False), (date(2005, 6, 25), False, False), @@ -163,26 +163,26 @@ def test_invalid_min_value(widget, value, exc, message): (date(2006, 7, 4), True, True), ], ) -def test_min_value_clip(widget, on_change_handler, min_value, clip_value, clip_max): +def test_min_clip(widget, on_change_handler, min, clip_value, clip_max): "If the current value or max is before a new min date, it is clipped" widget.value = date(2005, 6, 25) - widget.max_value = date(2005, 12, 31) + widget.max = date(2005, 12, 31) on_change_handler.reset_mock() - widget.min_value = min_value - assert widget.min_value == min_value + widget.min = min + assert widget.min == min if clip_value: - assert widget.value == min_value + assert widget.value == min on_change_handler.assert_called_once_with(widget) else: assert widget.value == date(2005, 6, 25) on_change_handler.assert_not_called() if clip_max: - assert widget.max_value == min_value + assert widget.max == min else: - assert widget.max_value == date(2005, 12, 31) + assert widget.max == date(2005, 12, 31) @pytest.mark.parametrize( @@ -194,24 +194,24 @@ def test_min_value_clip(widget, on_change_handler, min_value, clip_value, clip_m ("2023-03-11", date(2023, 3, 11)), ], ) -def test_max_value(widget, value, expected): - "The max_value of the datepicker can be set" - widget.max_value = value +def test_max(widget, value, expected): + "The max of the datepicker can be set" + widget.max = value - assert widget.max_value == expected + assert widget.max == expected @pytest.mark.parametrize("value, exc, message", INVALID_LIMITS) -def test_invalid_max_value(widget, value, exc, message): - "Invalid max_value values raise an exception" - widget.min_value = date(2015, 6, 12) +def test_invalid_max(widget, value, exc, message): + "Invalid max values raise an exception" + widget.min = date(2015, 6, 12) with pytest.raises(exc, match=message): - widget.max_value = value + widget.max = value @pytest.mark.parametrize( - "max_value, clip_value, clip_min", + "max, clip_value, clip_min", [ (date(2005, 6, 1), True, True), (date(2005, 6, 24), True, True), @@ -222,26 +222,26 @@ def test_invalid_max_value(widget, value, exc, message): (date(2006, 7, 4), False, False), ], ) -def test_max_value_clip(widget, on_change_handler, max_value, clip_value, clip_min): +def test_max_clip(widget, on_change_handler, max, clip_value, clip_min): "If the current value or min is after a new max date, it is clipped" - widget.min_value = date(2005, 6, 25) + widget.min = date(2005, 6, 25) widget.value = date(2005, 12, 31) on_change_handler.reset_mock() - widget.max_value = max_value - assert widget.max_value == max_value + widget.max = max + assert widget.max == max if clip_value: - assert widget.value == max_value + assert widget.value == max on_change_handler.assert_called_once_with(widget) else: assert widget.value == date(2005, 12, 31) on_change_handler.assert_not_called() if clip_min: - assert widget.min_value == max_value + assert widget.min == max else: - assert widget.min_value == date(2005, 6, 25) + assert widget.min == date(2005, 6, 25) def test_deprecated_names(): @@ -252,22 +252,35 @@ def test_deprecated_names(): DeprecationWarning, match="DatePicker has been renamed DateInput" ): widget = toga.DatePicker(min_date=MIN, max_date=MAX) - assert widget.min_value == MIN - assert widget.max_value == MAX - widget.min_value = widget.max_value = None + assert widget.min == MIN + assert widget.max == MAX + widget.min = widget.max = None - widget.min_date = MIN - assert widget.min_date == MIN - assert widget.min_value == MIN + with pytest.warns( + DeprecationWarning, match="DatePicker.min_date has been renamed DateInput.min" + ): + widget.min_date = MIN + assert widget.min_date == MIN + assert widget.min == MIN - widget.max_date = MAX - assert widget.max_date == MAX - assert widget.max_value == MAX + with pytest.warns( + DeprecationWarning, match="DatePicker.max_date has been renamed DateInput.max" + ): + widget.max_date = MAX + assert widget.max_date == MAX + assert widget.max == MAX with pytest.warns( DeprecationWarning, match="DatePicker has been renamed DateInput" ): widget = toga.DatePicker() - assert widget.min_date == date(1800, 1, 1) - assert widget.max_date == date(8999, 12, 31) + with pytest.warns( + DeprecationWarning, match="DatePicker.min_date has been renamed DateInput.min" + ): + assert widget.min_date == date(1800, 1, 1) + + with pytest.warns( + DeprecationWarning, match="DatePicker.max_date has been renamed DateInput.max" + ): + assert widget.max_date == date(8999, 12, 31) diff --git a/core/tests/widgets/test_timeinput.py b/core/tests/widgets/test_timeinput.py index 8a07a71488..d0c09ee59f 100644 --- a/core/tests/widgets/test_timeinput.py +++ b/core/tests/widgets/test_timeinput.py @@ -35,16 +35,16 @@ def test_widget_created_with_values(on_change_handler): # Round trip the impl/interface widget = toga.TimeInput( value=time(13, 37, 42), - min_value=time(6, 1, 2), - max_value=time(18, 58, 59), + min=time(6, 1, 2), + max=time(18, 58, 59), on_change=on_change_handler, ) assert widget._impl.interface == widget assert_action_performed(widget, "create TimeInput") assert widget.value == time(13, 37, 42) - assert widget.min_value == time(6, 1, 2) - assert widget.max_value == time(18, 58, 59) + assert widget.min == time(6, 1, 2) + assert widget.max == time(18, 58, 59) assert widget.on_change._raw == on_change_handler # The change handler isn't invoked at construction. @@ -100,8 +100,8 @@ def test_invalid_value(widget, value, exc, message): def test_value_clipping(widget, value, clipped, on_change_handler): "It the value is inconsistent with min/max, it is clipped." # Set min/max dates, and clear the on_change mock - widget.min_value = time(6, 0, 0) - widget.max_value = time(18, 0, 0) + widget.min = time(6, 0, 0) + widget.max = time(18, 0, 0) on_change_handler.reset_mock() # Set the new value @@ -123,24 +123,24 @@ def test_value_clipping(widget, value, clipped, on_change_handler): ("06:03:11", time(6, 3, 11)), ], ) -def test_min_value(widget, value, expected): - "The min_value of the datepicker can be set" - widget.min_value = value +def test_min(widget, value, expected): + "The min of the datepicker can be set" + widget.min = value - assert widget.min_value == expected + assert widget.min == expected @pytest.mark.parametrize("value, exc, message", INVALID_VALUES) -def test_invalid_min_value(widget, value, exc, message): - "Invalid min_value values raise an exception" - widget.max_value = time(18, 0, 0) +def test_invalid_min(widget, value, exc, message): + "Invalid min values raise an exception" + widget.max = time(18, 0, 0) with pytest.raises(exc, match=message): - widget.min_value = value + widget.min = value @pytest.mark.parametrize( - "min_value, clip_value, clip_max", + "min, clip_value, clip_max", [ (time(1, 2, 3), False, False), (time(3, 42, 37), False, False), @@ -151,26 +151,26 @@ def test_invalid_min_value(widget, value, exc, message): (time(13, 14, 15), True, True), ], ) -def test_min_value_clip(widget, on_change_handler, min_value, clip_value, clip_max): +def test_min_clip(widget, on_change_handler, min, clip_value, clip_max): "If the current value or max is before a new min time, it is clipped" widget.value = time(3, 42, 37) - widget.max_value = time(12, 0, 0) + widget.max = time(12, 0, 0) on_change_handler.reset_mock() - widget.min_value = min_value - assert widget.min_value == min_value + widget.min = min + assert widget.min == min if clip_value: - assert widget.value == min_value + assert widget.value == min on_change_handler.assert_called_once_with(widget) else: assert widget.value == time(3, 42, 37) on_change_handler.assert_not_called() if clip_max: - assert widget.max_value == min_value + assert widget.max == min else: - assert widget.max_value == time(12, 0, 0) + assert widget.max == time(12, 0, 0) @pytest.mark.parametrize( @@ -182,24 +182,24 @@ def test_min_value_clip(widget, on_change_handler, min_value, clip_value, clip_m ("18:03:11", time(18, 3, 11)), ], ) -def test_max_value(widget, value, expected): - "The max_value of the datepicker can be set" - widget.max_value = value +def test_max(widget, value, expected): + "The max of the datepicker can be set" + widget.max = value - assert widget.max_value == expected + assert widget.max == expected @pytest.mark.parametrize("value, exc, message", INVALID_VALUES) -def test_invalid_max_value(widget, value, exc, message): - "Invalid max_value values raise an exception" - widget.min_value = time(18, 0, 0) +def test_invalid_max(widget, value, exc, message): + "Invalid max values raise an exception" + widget.min = time(18, 0, 0) with pytest.raises(exc, match=message): - widget.max_value = value + widget.max = value @pytest.mark.parametrize( - "max_value, clip_value, clip_min", + "max, clip_value, clip_min", [ (time(1, 2, 3), True, True), (time(3, 42, 36), True, True), @@ -210,26 +210,26 @@ def test_invalid_max_value(widget, value, exc, message): (time(13, 14, 15), False, False), ], ) -def test_max_value_clip(widget, on_change_handler, max_value, clip_value, clip_min): +def test_max_clip(widget, on_change_handler, max, clip_value, clip_min): "If the current value is after a new max date, the value is clipped" - widget.min_value = time(3, 42, 37) + widget.min = time(3, 42, 37) widget.value = time(12, 0, 0) on_change_handler.reset_mock() - widget.max_value = max_value - assert widget.max_value == max_value + widget.max = max + assert widget.max == max if clip_value: - assert widget.value == max_value + assert widget.value == max on_change_handler.assert_called_once_with(widget) else: assert widget.value == time(12, 0, 0) on_change_handler.assert_not_called() if clip_min: - assert widget.min_value == max_value + assert widget.min == max else: - assert widget.min_value == time(3, 42, 37) + assert widget.min == time(3, 42, 37) def test_deprecated_names(): @@ -240,22 +240,39 @@ def test_deprecated_names(): DeprecationWarning, match="TimePicker has been renamed TimeInput" ): widget = toga.TimePicker(min_time=MIN, max_time=MAX) - assert widget.min_value == MIN - assert widget.max_value == MAX - widget.min_value = widget.max_value = None + assert widget.min == MIN + assert widget.max == MAX + widget.min = widget.max = None - widget.min_time = MIN - assert widget.min_time == MIN - assert widget.min_value == MIN + with pytest.warns( + DeprecationWarning, + match="TimePicker.min_time has been renamed TimeInput.min", + ): + widget.min_time = MIN + assert widget.min_time == MIN + assert widget.min == MIN - widget.max_time = MAX - assert widget.max_time == MAX - assert widget.max_value == MAX + with pytest.warns( + DeprecationWarning, + match="TimePicker.max_time has been renamed TimeInput.max", + ): + widget.max_time = MAX + assert widget.max_time == MAX + assert widget.max == MAX with pytest.warns( DeprecationWarning, match="TimePicker has been renamed TimeInput" ): widget = toga.TimePicker() - assert widget.min_time == time(0, 0, 0) - assert widget.max_time == time(23, 59, 59) + with pytest.warns( + DeprecationWarning, + match="TimePicker.min_time has been renamed TimeInput.min", + ): + assert widget.min_time == time(0, 0, 0) + + with pytest.warns( + DeprecationWarning, + match="TimePicker.max_time has been renamed TimeInput.max", + ): + assert widget.max_time == time(23, 59, 59) From 9c024dc78d2807484c982c26fdc2bc74f29a39bd Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 14:40:55 +0800 Subject: [PATCH 08/15] Modify numberinput to clip inconsistent min/max values. --- core/src/toga/widgets/numberinput.py | 10 ++++------ core/tests/widgets/test_numberinput.py | 22 ++++++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index b770aa6802..be31346830 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -197,10 +197,9 @@ def min(self, new_min): else: raise ValueError("min must be a number or None") + # Clip the max value if it's inconsistent with the new min if self.max is not None and new_min is not None and new_min > self.max: - raise ValueError( - f"min value of {new_min} is greater than the current max of {self.max}" - ) + self.max = new_min self._min = new_min self._impl.set_min_value(new_min) @@ -230,10 +229,9 @@ def max(self, new_max): else: raise ValueError("max must be a number or None") + # Clip the min value if it's inconsistent with the new max if self.min is not None and new_max is not None and new_max < self.min: - raise ValueError( - f"max value of {new_max} is less than the current min of {self.min}" - ) + self.min = new_max self._max = new_max self._impl.set_max_value(new_max) diff --git a/core/tests/widgets/test_numberinput.py b/core/tests/widgets/test_numberinput.py index 11d00b45cf..a9dbef3795 100644 --- a/core/tests/widgets/test_numberinput.py +++ b/core/tests/widgets/test_numberinput.py @@ -239,13 +239,12 @@ def test_bad_min(widget, value): def test_min_greater_than_max(widget): - "If the new min value exceeds the max value, an error is raised" + "If the new min value exceeds the max value, the max value is clipped" widget.max = 10 - with pytest.raises( - ValueError, - match=r"min value of 100.00 is greater than the current max of 10.00", - ): - widget.min = 100 + widget.min = 100 + + assert widget.max == 100 + assert widget.min == 100 @pytest.mark.parametrize(*QUANTIZE_PARAMS) @@ -309,13 +308,12 @@ def test_bad_max(widget, value): def test_max_less_than_min(widget): - "If the new max value is less than the min value, an error is raised" + "If the new max value is less than the min value, the min value is clipped" widget.min = 100 - with pytest.raises( - ValueError, - match=r"max value of 10.00 is less than the current min of 100.00", - ): - widget.max = 10 + widget.max = 10 + + assert widget.max == 10 + assert widget.min == 10 @pytest.mark.parametrize(*QUANTIZE_PARAMS) From 75891aa39b2780b19abbff42e0595d87f4c3cbeb Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 14:45:54 +0800 Subject: [PATCH 09/15] Removed unneeded changenote. --- changes/2004.misc.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changes/2004.misc.rst diff --git a/changes/2004.misc.rst b/changes/2004.misc.rst deleted file mode 100644 index 8e161207e2..0000000000 --- a/changes/2004.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Test coverage was restored for DateInput, TimeInput and Button widgets. From d74af1411d10d9d6440501594296d31305af4d68 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 15:00:54 +0800 Subject: [PATCH 10/15] Fixes for Winforms and Gtk --- gtk/src/toga_gtk/widgets/slider.py | 4 +-- testbed/tests/widgets/test_dateinput.py | 28 +++++++++----------- testbed/tests/widgets/test_textinput.py | 2 +- testbed/tests/widgets/test_timeinput.py | 20 +++++++------- winforms/src/toga_winforms/widgets/slider.py | 4 +-- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/gtk/src/toga_gtk/widgets/slider.py b/gtk/src/toga_gtk/widgets/slider.py index 08042915b4..36f3cec11d 100644 --- a/gtk/src/toga_gtk/widgets/slider.py +++ b/gtk/src/toga_gtk/widgets/slider.py @@ -59,7 +59,6 @@ def get_min(self): def set_min(self, value): self.adj.set_lower(value) - self.adj.set_upper(range[1]) def get_max(self): return self.adj.get_upper() @@ -71,7 +70,8 @@ def set_tick_count(self, tick_count): self.tick_count = tick_count self.native.clear_marks() if tick_count is not None: - min, max = self.get_range() + min = self.get_min() + max = self.get_max() span = max - min for i in range(tick_count): value = min + (span * (i / (tick_count - 1))) diff --git a/testbed/tests/widgets/test_dateinput.py b/testbed/tests/widgets/test_dateinput.py index b818d84089..d05158f8d5 100644 --- a/testbed/tests/widgets/test_dateinput.py +++ b/testbed/tests/widgets/test_dateinput.py @@ -91,12 +91,10 @@ async def test_init(): max = date(2000, 1, 1) on_change = Mock() - widget = toga.DateInput( - value=value, min_value=min, max_value=max, on_change=on_change - ) + widget = toga.DateInput(value=value, min=min, max=max, on_change=on_change) assert widget.value == value - assert widget.min_value == min - assert widget.max_value == max + assert widget.min == min + assert widget.max == max assert widget.on_change._raw is on_change @@ -121,9 +119,9 @@ async def test_value(widget, probe, normalize, assert_none_value, values, on_cha async def test_change(widget, probe, on_change): "The on_change handler is triggered on user input" - widget.min_value = date(2023, 5, 17) + widget.min = date(2023, 5, 17) widget.value = date(2023, 5, 20) - widget.max_value = date(2023, 5, 23) + widget.max = date(2023, 5, 23) on_change.reset_mock() @@ -135,9 +133,9 @@ async def test_change(widget, probe, on_change): assert on_change.mock_calls == [call(widget)] * i # Can't go past the maximum - assert widget.value == widget.max_value + assert widget.value == widget.max await probe.change(1) - assert widget.value == widget.max_value + assert widget.value == widget.max widget.value = date(2023, 5, 20) on_change.reset_mock() @@ -150,9 +148,9 @@ async def test_change(widget, probe, on_change): assert on_change.mock_calls == [call(widget)] * i # Can't go past the minimum - assert widget.value == widget.min_value + assert widget.value == widget.min await probe.change(-1) - assert widget.value == widget.min_value + assert widget.value == widget.min async def test_min(widget, probe, initial_value, min_value, values, normalize): @@ -162,8 +160,8 @@ async def test_min(widget, probe, initial_value, min_value, values, normalize): assert probe.min_value == min_value for min in values: - widget.min_value = min - assert widget.min_value == min + widget.min = min + assert widget.min == min if value < min: value = normalize(min) @@ -181,8 +179,8 @@ async def test_max(widget, probe, initial_value, max_value, values, normalize): assert probe.max_value == max_value for max in reversed(values): - widget.max_value = max - assert widget.max_value == max + widget.max = max + assert widget.max == max if value > max: value = normalize(max) diff --git a/testbed/tests/widgets/test_textinput.py b/testbed/tests/widgets/test_textinput.py index 72ea9d7c45..a507939d1f 100644 --- a/testbed/tests/widgets/test_textinput.py +++ b/testbed/tests/widgets/test_textinput.py @@ -94,7 +94,7 @@ async def test_on_change_user(widget, probe, on_change): for count, char in enumerate("Hello world", start=1): await probe.type_character(char) - await probe.redraw(f"Typed {char!r}") + await probe.redraw(f"Typed {char!r}", delay=0.02) # The number of events equals the number of characters typed. assert on_change.mock_calls == [call(widget)] * count diff --git a/testbed/tests/widgets/test_timeinput.py b/testbed/tests/widgets/test_timeinput.py index 92a27ec4de..a15ff14229 100644 --- a/testbed/tests/widgets/test_timeinput.py +++ b/testbed/tests/widgets/test_timeinput.py @@ -87,12 +87,10 @@ async def test_init(normalize): max = time(20, 30, 40) on_change = Mock() - widget = toga.TimeInput( - value=value, min_value=min, max_value=max, on_change=on_change - ) + widget = toga.TimeInput(value=value, min=min, max=max, on_change=on_change) assert widget.value == normalize(value) - assert widget.min_value == min - assert widget.max_value == max + assert widget.min == min + assert widget.max == max assert widget.on_change._raw is on_change @@ -101,9 +99,9 @@ async def test_change(widget, probe, on_change): # The probe `change` method operates on minutes, because not all backends support # seconds. - widget.min_value = time(5, 7) + widget.min = time(5, 7) widget.value = time(5, 10) - widget.max_value = time(5, 13) + widget.max = time(5, 13) on_change.reset_mock() for i in range(1, 4): @@ -114,9 +112,9 @@ async def test_change(widget, probe, on_change): assert on_change.mock_calls == [call(widget)] * i # Can't go past the maximum - assert widget.value == widget.max_value + assert widget.value == widget.max await probe.change(1) - assert widget.value == widget.max_value + assert widget.value == widget.max widget.value = time(5, 10) on_change.reset_mock() @@ -129,6 +127,6 @@ async def test_change(widget, probe, on_change): assert on_change.mock_calls == [call(widget)] * i # Can't go past the minimum - assert widget.value == widget.min_value + assert widget.value == widget.min await probe.change(-1) - assert widget.value == widget.min_value + assert widget.value == widget.min diff --git a/winforms/src/toga_winforms/widgets/slider.py b/winforms/src/toga_winforms/widgets/slider.py index 1ec1b64c9a..0220d4dfb9 100644 --- a/winforms/src/toga_winforms/widgets/slider.py +++ b/winforms/src/toga_winforms/widgets/slider.py @@ -37,8 +37,8 @@ def set_int_value(self, value): def get_int_max(self): return self.native.Maximum - def set_int_max(self, max): - self.native.Maximum = max + def set_int_max(self, value): + self.native.Maximum = value def set_ticks_visible(self, visible): self.native.TickStyle = BOTTOM_RIGHT_TICK_STYLE if visible else NONE_TICK_STYLE From 8b03ddd860909cde1e73181c3122393b34718309 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 15:26:31 +0800 Subject: [PATCH 11/15] Fix typo in docstring. --- core/src/toga/widgets/slider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index d4f824b6ba..4f4e0a056a 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -158,7 +158,7 @@ def max(self) -> float: """Maximum allowed value. If the new maximum value is less than the current minimum value, - the minimum value will be decreaed to the new maximum value. + the minimum value will be decreased to the new maximum value. """ return self._impl.get_max() From 187221a975e2cbd35e9b783140db05a39bf64c3e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 15:35:55 +0800 Subject: [PATCH 12/15] Rework changenotes for Date/TimeInput. --- changes/1951.removal.2.rst | 2 +- changes/1951.removal.3.rst | 1 - changes/1999.removal.3.rst | 1 + changes/{1951.removal.4.rst => 1999.removal.4.rst} | 0 4 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 changes/1951.removal.3.rst create mode 100644 changes/1999.removal.3.rst rename changes/{1951.removal.4.rst => 1999.removal.4.rst} (100%) diff --git a/changes/1951.removal.2.rst b/changes/1951.removal.2.rst index 8e47bd7180..697d6e1f85 100644 --- a/changes/1951.removal.2.rst +++ b/changes/1951.removal.2.rst @@ -1 +1 @@ -``DatePicker.min_date`` and ``DatePicker.max_date`` has been renamed ``DateInput.min`` and ``DateInput.max``, respectively. +``TimePicker`` has been renamed ``TimeInput``. diff --git a/changes/1951.removal.3.rst b/changes/1951.removal.3.rst deleted file mode 100644 index 697d6e1f85..0000000000 --- a/changes/1951.removal.3.rst +++ /dev/null @@ -1 +0,0 @@ -``TimePicker`` has been renamed ``TimeInput``. diff --git a/changes/1999.removal.3.rst b/changes/1999.removal.3.rst new file mode 100644 index 0000000000..8e47bd7180 --- /dev/null +++ b/changes/1999.removal.3.rst @@ -0,0 +1 @@ +``DatePicker.min_date`` and ``DatePicker.max_date`` has been renamed ``DateInput.min`` and ``DateInput.max``, respectively. diff --git a/changes/1951.removal.4.rst b/changes/1999.removal.4.rst similarity index 100% rename from changes/1951.removal.4.rst rename to changes/1999.removal.4.rst From de1b98bb748a9109ba73aa48266ded2a3922f0d7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 21 Jun 2023 15:36:09 +0800 Subject: [PATCH 13/15] Tweak docstrings and warnings for slider. --- core/src/toga/widgets/slider.py | 6 +++++- core/tests/widgets/test_slider.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 4f4e0a056a..a33ed8f6f0 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -60,7 +60,7 @@ def __init__( ) min, max = range else: - # This inserts defau. + # This provides defaults values for min/max. if min is None: min = 0.0 if max is None: @@ -296,6 +296,10 @@ def range(self) -> tuple[float, float]: @range.setter def range(self, range): + warnings.warn( + "Slider.range has been deprecated in favor of Slider.min and Slider.max", + DeprecationWarning, + ) _min, _max = range self.min = _min self.max = _max diff --git a/core/tests/widgets/test_slider.py b/core/tests/widgets/test_slider.py index 69599c29cc..6079c0e5b8 100644 --- a/core/tests/widgets/test_slider.py +++ b/core/tests/widgets/test_slider.py @@ -545,7 +545,11 @@ def test_deprecated(): assert widget.range == (pytest.approx(2), pytest.approx(4)) # range is converted to min/max - widget.range = (6, 8) + with pytest.warns( + DeprecationWarning, + match="Slider.range has been deprecated in favor of Slider.min and Slider.max", + ): + widget.range = (6, 8) assert widget.min == pytest.approx(6) assert widget.max == pytest.approx(8) From 7069297a9f41d3924035437a9d85b876d316eb8e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 22 Jun 2023 07:22:46 +0800 Subject: [PATCH 14/15] Cleanups following code review. --- core/src/toga/widgets/numberinput.py | 54 +++++++++++++++++-------- core/src/toga/widgets/slider.py | 37 +++++++++++------ core/tests/widgets/test_dateinput.py | 5 +++ core/tests/widgets/test_timeinput.py | 6 +++ iOS/src/toga_iOS/widgets/slider.py | 12 +++++- testbed/tests/widgets/test_slider.py | 16 ++++---- testbed/tests/widgets/test_textinput.py | 3 ++ 7 files changed, 95 insertions(+), 38 deletions(-) diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index be31346830..d47bf10c7c 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -80,18 +80,22 @@ def __init__( Inherits from :class:`~toga.widgets.base.Widget`. :param id: The ID for the widget. - :param style: A style object. If no style is provided, a default style - will be applied to the widget. - :param step: The amount that any increment/decrement operations will - apply to the widget's current value. - :param min: If provided, ``value`` will be guaranteed to - be greater than or equal to this minimum. - :param max: If provided, ``value`` will be guaranteed to - be less than or equal to this maximum. + :param style: A style object. If no style is provided, a default style will be + applied to the widget. + :param step: The amount that any increment/decrement operations will apply to + the widget's current value. + :param min: If provided, :any:`value` will be guaranteed to be greater than or + equal to this minimum. + :param max: If provided, :any:`value` will be guaranteed to be less than or + equal to this maximum. :param value: The initial value for the widget. :param readonly: Can the value of the widget be modified by the user? - :param on_change: A handler that will be invoked when the the value of - the widget changes. + :param on_change: A handler that will be invoked when the the value of the + widget changes. + :param min_value: **DEPRECATED**; use :any`min`. If provided, :any`value` will + be guaranteed to be greater than or equal to this minimum. + :param max_value: **DEPRECATED**; use :any:`max`. If provided, :any:`value` will + be guaranteed to be less than or equal to this maximum. """ super().__init__(id=id, style=style) @@ -176,10 +180,10 @@ def step(self, step): def min(self) -> Decimal | None: """The minimum bound for the widget's value. - Returns ``None`` if there is no minimum bound. + Returns :any:`None` if there is no minimum bound. - If the current ``value`` is less than a newly specified ``min``, - ``value`` will be clipped to conform to the new minimum. + When setting this property, the current :attr:`value` and :attr:`max` will be + clipped to the to the new minimum value. """ return self._min @@ -208,10 +212,10 @@ def min(self, new_min): def max(self) -> Decimal | None: """The maximum bound for the widget's value. - Returns ``None`` if there is no maximum bound. + Returns :any:`None` if there is no maximum bound. - If the current ``value`` exceeds a newly specified ``max``, - ``value`` will be clipped to conform to the new maximum. + When setting this property, the current :attr:`value` and :attr:`min` will be + clipped to the to the new maximum value. """ return self._max @@ -295,6 +299,15 @@ def on_change(self, handler): @property def min_value(self) -> Decimal | None: + """The minimum bound for the widget's value. + + **DEPRECATED**; use :attr:`min`. + + Returns :any:`None` if there is no minimum bound. + + When setting this property, the current :attr:`value` and :attr:`max` will be + clipped to the to the new minimum value. + """ warnings.warn( "NumberInput.min_value has been renamed NumberInput.min", DeprecationWarning, @@ -311,6 +324,15 @@ def min_value(self, value): @property def max_value(self) -> Decimal | None: + """The maximum bound for the widget's value. + + **DEPRECATED**; use :attr:`max`. + + Returns :any:`None` if there is no maximum bound. + + When setting this property, the current :attr:`value` and :attr:`min` will be + clipped to the to the new maximum value. + """ warnings.warn( "NumberInput.max_value has been renamed NumberInput.max", DeprecationWarning, diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index a33ed8f6f0..6e445caeb5 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -29,18 +29,20 @@ def __init__( Inherits from :class:`~toga.widgets.base.Widget`. :param id: The ID for the widget. - :param style: A style object. If no style is provided, a default style - will be applied to the widget. - :param value: Initial :any:`value` of the slider. Defaults to the - mid-point of the range. - :param range: Initial :any:`range` range of the slider. Defaults to ``(0, - 1)``. - :param tick_count: Initial :any:`tick_count` for the slider. If ``None``, - the slider will be continuous. + :param style: A style object. If no style is provided, a default style will be + applied to the widget. + :param value: Initial :any:`value` of the slider. Defaults to the mid-point of + the range. + :param min: Initial minimum value of the slider. Defaults to 0. + :param max: Initial maximum value of the slider. Defaults to 1. + :param tick_count: Initial :any:`tick_count` for the slider. If :any:`None`, the + slider will be continuous. :param on_change: Initial :any:`on_change` handler. :param on_press: Initial :any:`on_press` handler. :param on_release: Initial :any:`on_release` handler. :param enabled: Whether the user can interact with the widget. + :param range: **DEPRECATED**; use :any:`min` and :any:`max`. Initial + :any:`range` range of the slider. Defaults to ``(0, 1)``. """ super().__init__(id=id, style=style) self._impl = self.factory.Slider(interface=self) @@ -133,8 +135,8 @@ def _round_value(self, value): def min(self) -> float: """Minimum allowed value. - If the new minimum value is greater than the current maximum value, - the maximum value will be increased to the new minimum value. + When setting this property, the current :attr:`value` and :attr:`max` will be + clipped to the to the new minimum value. """ return self._impl.get_min() @@ -157,8 +159,8 @@ def min(self, value): def max(self) -> float: """Maximum allowed value. - If the new maximum value is less than the current minimum value, - the minimum value will be decreased to the new maximum value. + When setting this property, the current :attr:`value` and :attr:`min` will be + clipped to the to the new maximum value. """ return self._impl.get_max() @@ -288,6 +290,17 @@ def on_release(self, handler): ###################################################################### @property def range(self) -> tuple[float, float]: + """Range of allowed values, in the form (min, max). + + **DEPRECATED**; use ``Slider.min`` and ``Slider.max``. + + If the provided min is greater than the max, both values will assume the value + of the max. + + If the current value is less than the provided ``min``, the current value will + be clipped to the minimum value. If the current value is greater than the + provided ``max``, the current value will be clipped than the maximum value. + """ warnings.warn( "Slider.range has been deprecated in favor of Slider.min and Slider.max", DeprecationWarning, diff --git a/core/tests/widgets/test_dateinput.py b/core/tests/widgets/test_dateinput.py index fb7181f0f7..6ab4be73cf 100644 --- a/core/tests/widgets/test_dateinput.py +++ b/core/tests/widgets/test_dateinput.py @@ -267,7 +267,12 @@ def test_deprecated_names(): DeprecationWarning, match="DatePicker.max_date has been renamed DateInput.max" ): widget.max_date = MAX + + with pytest.warns( + DeprecationWarning, match="DatePicker.max_date has been renamed DateInput.max" + ): assert widget.max_date == MAX + assert widget.max == MAX with pytest.warns( diff --git a/core/tests/widgets/test_timeinput.py b/core/tests/widgets/test_timeinput.py index d0c09ee59f..9a574ec61f 100644 --- a/core/tests/widgets/test_timeinput.py +++ b/core/tests/widgets/test_timeinput.py @@ -257,7 +257,13 @@ def test_deprecated_names(): match="TimePicker.max_time has been renamed TimeInput.max", ): widget.max_time = MAX + + with pytest.warns( + DeprecationWarning, + match="TimePicker.max_time has been renamed TimeInput.max", + ): assert widget.max_time == MAX + assert widget.max == MAX with pytest.warns( diff --git a/iOS/src/toga_iOS/widgets/slider.py b/iOS/src/toga_iOS/widgets/slider.py index c2928087bb..a6cec491e5 100644 --- a/iOS/src/toga_iOS/widgets/slider.py +++ b/iOS/src/toga_iOS/widgets/slider.py @@ -51,6 +51,8 @@ def create(self): # Dummy values used during initialization. self.value = 0 + self.min_value = 0 + self.max_value = 1 self.tick_count = None self.native.addTarget( @@ -82,15 +84,21 @@ def set_value(self, value): self.native.setValue(value, animated=True) def get_min(self): - return self.native.minimumValue + # Use the shadow copy, not the native value, to ensure round tripping. + # See implementation notes for details. + return self.min_value def set_min(self, value): + self.min_value = value self.native.minimumValue = value def get_max(self): - return self.native.maximumValue + # Use the shadow copy, not the native value, to ensure round tripping. + # See implementation notes for details. + return self.max_value def set_max(self, value): + self.max_value = value self.native.maximumValue = value def get_tick_count(self): diff --git a/testbed/tests/widgets/test_slider.py b/testbed/tests/widgets/test_slider.py index fb7dbe9187..07a5725217 100644 --- a/testbed/tests/widgets/test_slider.py +++ b/testbed/tests/widgets/test_slider.py @@ -80,7 +80,7 @@ async def test_value(widget, probe, on_change): def assert_set_value(widget, value_in, value_out=None): if value_out is None: value_out = value_in - value_out = assert_set_get(widget, "value", value_in, pytest.approx(value_out)) + value_out = assert_set_get(widget, "value", value_in, value_out) assert isinstance(value_out, float) @@ -106,17 +106,17 @@ async def test_change(widget, probe, on_change): async def test_min(widget, probe, on_change): for min in POSITIONS[:-1]: on_change.reset_mock() - min_out = assert_set_get(widget, "min", min, expected=pytest.approx(min)) + min_out = assert_set_get(widget, "min", min) assert isinstance(min_out, float) if min <= 0.5: # The existing value is in the range, so it should not change. - assert widget.value == pytest.approx(0.5) + assert widget.value == 0.5 assert probe.position == approx((0.5 - min) / (1 - min), abs=ACCURACY) on_change.assert_not_called() else: # The existing value is out of the range, so it should be clipped. - assert widget.value == pytest.approx(min) + assert widget.value == min assert probe.position == 0 on_change.assert_called_once_with(widget) await probe.redraw("Slider min property should be %s" % min) @@ -127,17 +127,17 @@ async def test_max(widget, probe, on_change): # If the existing value is in the range, it should not change. for max in POSITIONS[-1:0:-1]: on_change.reset_mock() - max_out = assert_set_get(widget, "max", max, expected=pytest.approx(max)) + max_out = assert_set_get(widget, "max", max) assert isinstance(max_out, float) if max >= 0.5: # The existing value is in the range, so it should not change. - assert widget.value == pytest.approx(0.5) + assert widget.value == 0.5 assert probe.position == approx(0.5 / max, abs=ACCURACY) on_change.assert_not_called() else: # The existing value is out of the range, so it should be clipped. - assert widget.value == pytest.approx(max) + assert widget.value == max assert probe.position == 1 on_change.assert_called_once_with(widget) await probe.redraw("Slider max property should be %s" % max) @@ -219,7 +219,7 @@ async def test_range_with_ticks(widget, probe, on_change): on_change.reset_mock() widget.min = min widget.max = max - assert widget.value == pytest.approx(value) + assert widget.value == value assert probe.position == approx((value - min) / (max - min), abs=ACCURACY) if value == prev_value: diff --git a/testbed/tests/widgets/test_textinput.py b/testbed/tests/widgets/test_textinput.py index a507939d1f..58713716cf 100644 --- a/testbed/tests/widgets/test_textinput.py +++ b/testbed/tests/widgets/test_textinput.py @@ -94,6 +94,9 @@ async def test_on_change_user(widget, probe, on_change): for count, char in enumerate("Hello world", start=1): await probe.type_character(char) + # GTK has an intermittent failure because on_change handler + # caused by typing a character doesn't fully propegate. A + # short delay fixes this. await probe.redraw(f"Typed {char!r}", delay=0.02) # The number of events equals the number of characters typed. From fccfb55cc3eedb4b3e08f38a39a0c4bec906c65b Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 22 Jun 2023 17:57:40 +0100 Subject: [PATCH 15/15] Minor cleanups --- core/src/toga/widgets/dateinput.py | 6 ++++-- core/src/toga/widgets/numberinput.py | 30 ++++++---------------------- core/src/toga/widgets/slider.py | 14 ++++++------- core/src/toga/widgets/timeinput.py | 6 ++++-- core/tests/widgets/test_dateinput.py | 17 ++++++---------- core/tests/widgets/test_timeinput.py | 28 +++++++++----------------- testbed/tests/widgets/test_slider.py | 4 ++-- 7 files changed, 38 insertions(+), 67 deletions(-) diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index f4b5130ade..55f2c5847f 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -100,7 +100,8 @@ def min(self) -> datetime.date: """The minimum allowable date (inclusive). A value of ``None`` will be converted into the lowest supported date of 1800-01-01. - The existing ``value`` and ``max`` will be clipped to the new minimum. + When setting this property, the current :attr:`value` and :attr:`max` will be + clipped against the new minimum value. :raises ValueError: If set to a date outside of the supported range. """ @@ -124,7 +125,8 @@ def max(self) -> datetime.date: """The maximum allowable date (inclusive). A value of ``None`` will be converted into the highest supported date of 8999-12-31. - The existing ``value`` and ``min`` will be clipped to the new maximum. + When setting this property, the current :attr:`value` and :attr:`min` will be + clipped against the new maximum value. :raises ValueError: If set to a date outside of the supported range. """ diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index d47bf10c7c..b4bf541316 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -92,10 +92,8 @@ def __init__( :param readonly: Can the value of the widget be modified by the user? :param on_change: A handler that will be invoked when the the value of the widget changes. - :param min_value: **DEPRECATED**; use :any`min`. If provided, :any`value` will - be guaranteed to be greater than or equal to this minimum. - :param max_value: **DEPRECATED**; use :any:`max`. If provided, :any:`value` will - be guaranteed to be less than or equal to this maximum. + :param min_value: **DEPRECATED**; alias of ``min``. + :param max_value: **DEPRECATED**; alias of ``max``. """ super().__init__(id=id, style=style) @@ -183,7 +181,7 @@ def min(self) -> Decimal | None: Returns :any:`None` if there is no minimum bound. When setting this property, the current :attr:`value` and :attr:`max` will be - clipped to the to the new minimum value. + clipped against the new minimum value. """ return self._min @@ -215,7 +213,7 @@ def max(self) -> Decimal | None: Returns :any:`None` if there is no maximum bound. When setting this property, the current :attr:`value` and :attr:`min` will be - clipped to the to the new maximum value. + clipped against the new maximum value. """ return self._max @@ -299,15 +297,7 @@ def on_change(self, handler): @property def min_value(self) -> Decimal | None: - """The minimum bound for the widget's value. - - **DEPRECATED**; use :attr:`min`. - - Returns :any:`None` if there is no minimum bound. - - When setting this property, the current :attr:`value` and :attr:`max` will be - clipped to the to the new minimum value. - """ + """**DEPRECATED**; alias of :attr:`min`.""" warnings.warn( "NumberInput.min_value has been renamed NumberInput.min", DeprecationWarning, @@ -324,15 +314,7 @@ def min_value(self, value): @property def max_value(self) -> Decimal | None: - """The maximum bound for the widget's value. - - **DEPRECATED**; use :attr:`max`. - - Returns :any:`None` if there is no maximum bound. - - When setting this property, the current :attr:`value` and :attr:`min` will be - clipped to the to the new maximum value. - """ + """**DEPRECATED**; alias of :attr:`max`.""" warnings.warn( "NumberInput.max_value has been renamed NumberInput.max", DeprecationWarning, diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 6e445caeb5..d8156cfec6 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -41,8 +41,8 @@ def __init__( :param on_press: Initial :any:`on_press` handler. :param on_release: Initial :any:`on_release` handler. :param enabled: Whether the user can interact with the widget. - :param range: **DEPRECATED**; use :any:`min` and :any:`max`. Initial - :any:`range` range of the slider. Defaults to ``(0, 1)``. + :param range: **DEPRECATED**; use ``min`` and ``max`` instead. Initial + :any:`range` of the slider. Defaults to ``(0, 1)``. """ super().__init__(id=id, style=style) self._impl = self.factory.Slider(interface=self) @@ -136,7 +136,7 @@ def min(self) -> float: """Minimum allowed value. When setting this property, the current :attr:`value` and :attr:`max` will be - clipped to the to the new minimum value. + clipped against the new minimum value. """ return self._impl.get_min() @@ -160,7 +160,7 @@ def max(self) -> float: """Maximum allowed value. When setting this property, the current :attr:`value` and :attr:`min` will be - clipped to the to the new maximum value. + clipped against the new maximum value. """ return self._impl.get_max() @@ -290,16 +290,16 @@ def on_release(self, handler): ###################################################################### @property def range(self) -> tuple[float, float]: - """Range of allowed values, in the form (min, max). + """**DEPRECATED**; use :any:`min` and :any:`max` instead. - **DEPRECATED**; use ``Slider.min`` and ``Slider.max``. + Range of allowed values, in the form (min, max). If the provided min is greater than the max, both values will assume the value of the max. If the current value is less than the provided ``min``, the current value will be clipped to the minimum value. If the current value is greater than the - provided ``max``, the current value will be clipped than the maximum value. + provided ``max``, the current value will be clipped to the maximum value. """ warnings.warn( "Slider.range has been deprecated in favor of Slider.min and Slider.max", diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 8a0342eac8..fde2d64583 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -83,7 +83,8 @@ def min(self) -> datetime.time: """The minimum allowable time (inclusive). A value of ``None`` will be converted into 00:00:00. - The existing ``value`` and ``max`` will be clipped to the new minimum. + When setting this property, the current :attr:`value` and :attr:`max` will be + clipped against the new minimum value. """ return self._impl.get_min_time() @@ -105,7 +106,8 @@ def max(self) -> datetime.time: """The maximum allowable time (inclusive). A value of ``None`` will be converted into 23:59:59. - The existing ``value`` and ``min`` will be clipped to the new maximum. + When setting this property, the current :attr:`value` and :attr:`min` will be + clipped against the new maximum value. """ return self._impl.get_max_time() diff --git a/core/tests/widgets/test_dateinput.py b/core/tests/widgets/test_dateinput.py index 6ab4be73cf..0c6beca0f0 100644 --- a/core/tests/widgets/test_dateinput.py +++ b/core/tests/widgets/test_dateinput.py @@ -260,6 +260,10 @@ def test_deprecated_names(): DeprecationWarning, match="DatePicker.min_date has been renamed DateInput.min" ): widget.min_date = MIN + + with pytest.warns( + DeprecationWarning, match="DatePicker.min_date has been renamed DateInput.min" + ): assert widget.min_date == MIN assert widget.min == MIN @@ -272,20 +276,11 @@ def test_deprecated_names(): DeprecationWarning, match="DatePicker.max_date has been renamed DateInput.max" ): assert widget.max_date == MAX - assert widget.max == MAX with pytest.warns( DeprecationWarning, match="DatePicker has been renamed DateInput" ): widget = toga.DatePicker() - - with pytest.warns( - DeprecationWarning, match="DatePicker.min_date has been renamed DateInput.min" - ): - assert widget.min_date == date(1800, 1, 1) - - with pytest.warns( - DeprecationWarning, match="DatePicker.max_date has been renamed DateInput.max" - ): - assert widget.max_date == date(8999, 12, 31) + assert widget.min == date(1800, 1, 1) + assert widget.max == date(8999, 12, 31) diff --git a/core/tests/widgets/test_timeinput.py b/core/tests/widgets/test_timeinput.py index 9a574ec61f..e3c4871316 100644 --- a/core/tests/widgets/test_timeinput.py +++ b/core/tests/widgets/test_timeinput.py @@ -245,40 +245,30 @@ def test_deprecated_names(): widget.min = widget.max = None with pytest.warns( - DeprecationWarning, - match="TimePicker.min_time has been renamed TimeInput.min", + DeprecationWarning, match="TimePicker.min_time has been renamed TimeInput.min" ): widget.min_time = MIN + + with pytest.warns( + DeprecationWarning, match="TimePicker.min_time has been renamed TimeInput.min" + ): assert widget.min_time == MIN assert widget.min == MIN with pytest.warns( - DeprecationWarning, - match="TimePicker.max_time has been renamed TimeInput.max", + DeprecationWarning, match="TimePicker.max_time has been renamed TimeInput.max" ): widget.max_time = MAX with pytest.warns( - DeprecationWarning, - match="TimePicker.max_time has been renamed TimeInput.max", + DeprecationWarning, match="TimePicker.max_time has been renamed TimeInput.max" ): assert widget.max_time == MAX - assert widget.max == MAX with pytest.warns( DeprecationWarning, match="TimePicker has been renamed TimeInput" ): widget = toga.TimePicker() - - with pytest.warns( - DeprecationWarning, - match="TimePicker.min_time has been renamed TimeInput.min", - ): - assert widget.min_time == time(0, 0, 0) - - with pytest.warns( - DeprecationWarning, - match="TimePicker.max_time has been renamed TimeInput.max", - ): - assert widget.max_time == time(23, 59, 59) + assert widget.min == time(0, 0, 0) + assert widget.max == time(23, 59, 59) diff --git a/testbed/tests/widgets/test_slider.py b/testbed/tests/widgets/test_slider.py index 07a5725217..f94ad70c34 100644 --- a/testbed/tests/widgets/test_slider.py +++ b/testbed/tests/widgets/test_slider.py @@ -102,7 +102,7 @@ async def test_change(widget, probe, on_change): await probe.redraw("Slider scale should be reset") -# Bounds checks and the `min` property are covered by the core tests. +# All other aspects of this property are covered by the core tests. async def test_min(widget, probe, on_change): for min in POSITIONS[:-1]: on_change.reset_mock() @@ -122,7 +122,7 @@ async def test_min(widget, probe, on_change): await probe.redraw("Slider min property should be %s" % min) -# Bounds checks and the `max` property are covered by the core tests. +# All other aspects of this property are covered by the core tests. async def test_max(widget, probe, on_change): # If the existing value is in the range, it should not change. for max in POSITIONS[-1:0:-1]: