From 0ca1da74145e6c070a01ffbe30077a85404d196e Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Tue, 5 Nov 2024 01:29:35 -0500 Subject: [PATCH 01/15] Restructured widget initialization order --- cocoa/src/toga_cocoa/widgets/base.py | 2 - core/2937.misc.rst | 1 + core/src/toga/style/applicator.py | 50 ++++++++++--------- core/src/toga/widgets/activityindicator.py | 4 +- core/src/toga/widgets/base.py | 25 ++++++++-- core/src/toga/widgets/box.py | 5 +- core/src/toga/widgets/button.py | 5 +- core/src/toga/widgets/canvas.py | 5 +- core/src/toga/widgets/dateinput.py | 5 +- core/src/toga/widgets/detailedlist.py | 22 ++++---- core/src/toga/widgets/divider.py | 4 +- core/src/toga/widgets/imageview.py | 7 ++- core/src/toga/widgets/label.py | 5 +- core/src/toga/widgets/mapview.py | 6 ++- core/src/toga/widgets/multilinetextinput.py | 5 +- core/src/toga/widgets/numberinput.py | 3 +- core/src/toga/widgets/optioncontainer.py | 4 +- core/src/toga/widgets/passwordinput.py | 3 +- core/src/toga/widgets/progressbar.py | 4 +- core/src/toga/widgets/scrollcontainer.py | 5 +- core/src/toga/widgets/selection.py | 3 +- core/src/toga/widgets/slider.py | 3 +- core/src/toga/widgets/splitcontainer.py | 5 +- core/src/toga/widgets/switch.py | 4 +- core/src/toga/widgets/table.py | 7 +-- core/src/toga/widgets/textinput.py | 8 +-- core/src/toga/widgets/timeinput.py | 5 +- core/src/toga/widgets/tree.py | 7 +-- core/src/toga/widgets/webview.py | 3 +- core/tests/app/test_widget_registry.py | 1 - core/tests/style/pack/utils.py | 20 ++++++-- core/tests/style/test_applicator.py | 7 ++- core/tests/widgets/test_base.py | 16 +++++- .../window/test_filtered_widget_registry.py | 1 - dummy/src/toga_dummy/widgets/base.py | 1 - gtk/src/toga_gtk/widgets/base.py | 4 -- iOS/src/toga_iOS/widgets/base.py | 2 - textual/src/toga_textual/widgets/base.py | 1 - web/src/toga_web/widgets/base.py | 1 - winforms/src/toga_winforms/widgets/base.py | 2 - 40 files changed, 153 insertions(+), 118 deletions(-) create mode 100644 core/2937.misc.rst diff --git a/cocoa/src/toga_cocoa/widgets/base.py b/cocoa/src/toga_cocoa/widgets/base.py index 28da6271fb..06e6264135 100644 --- a/cocoa/src/toga_cocoa/widgets/base.py +++ b/cocoa/src/toga_cocoa/widgets/base.py @@ -9,12 +9,10 @@ class Widget: def __init__(self, interface): super().__init__() self.interface = interface - self.interface._impl = self self._container = None self.constraints = None self.native = None self.create() - self.interface.style.reapply() @abstractmethod def create(self): ... diff --git a/core/2937.misc.rst b/core/2937.misc.rst new file mode 100644 index 0000000000..41bb0166c3 --- /dev/null +++ b/core/2937.misc.rst @@ -0,0 +1 @@ +The initialization process for widgets has been internally restructured to avoid unnecessary style reapplications. diff --git a/core/src/toga/style/applicator.py b/core/src/toga/style/applicator.py index 2b0c17ee75..4545d79d8e 100644 --- a/core/src/toga/style/applicator.py +++ b/core/src/toga/style/applicator.py @@ -1,38 +1,42 @@ -from __future__ import annotations +import warnings -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from toga.widgets.base import Widget +# Make sure deprecation warnings are shown by default +warnings.filterwarnings("default", category=DeprecationWarning) class TogaApplicator: """Apply styles to a Toga widget.""" - def __init__(self, widget: Widget): - self.widget = widget + def __init__(self, widget: None = None): + if widget is not None: + warnings.warn( + "Widget parameter is deprecated. Applicator will be given a reference " + "to its widget when it is assigned as that widget's applicator.", + DeprecationWarning, + stacklevel=2, + ) def refresh(self) -> None: - # print("RE-EVALUATE LAYOUT", self.widget) - self.widget.refresh() + # print("RE-EVALUATE LAYOUT", self.node) + self.node.refresh() def set_bounds(self) -> None: - # print(" APPLY LAYOUT", self.widget, self.widget.layout) - self.widget._impl.set_bounds( - self.widget.layout.absolute_content_left, - self.widget.layout.absolute_content_top, - self.widget.layout.content_width, - self.widget.layout.content_height, + # print(" APPLY LAYOUT", self.node, self.node.layout) + self.node._impl.set_bounds( + self.node.layout.absolute_content_left, + self.node.layout.absolute_content_top, + self.node.layout.content_width, + self.node.layout.content_height, ) - for child in self.widget.children: + for child in self.node.children: child.applicator.set_bounds() def set_text_alignment(self, alignment: str) -> None: - self.widget._impl.set_alignment(alignment) + self.node._impl.set_alignment(alignment) def set_hidden(self, hidden: bool) -> None: - self.widget._impl.set_hidden(hidden) - for child in self.widget.children: + self.node._impl.set_hidden(hidden) + for child in self.node.children: # If the parent is hidden, then so are all children. However, if the # parent is visible, then the child's explicit visibility style is # taken into account. This visibility cascades into any @@ -49,11 +53,11 @@ def set_hidden(self, hidden: bool) -> None: def set_font(self, font: object) -> None: # Changing the font of a widget can make the widget change size, # which in turn means we need to do a re-layout - self.widget._impl.set_font(font) - self.widget.refresh() + self.node._impl.set_font(font) + self.node.refresh() def set_color(self, color: object) -> None: - self.widget._impl.set_color(color) + self.node._impl.set_color(color) def set_background_color(self, color: object) -> None: - self.widget._impl.set_background_color(color) + self.node._impl.set_background_color(color) diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index 102f44d129..7bc731f7fb 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -6,6 +6,8 @@ class ActivityIndicator(Widget): + _IMPL_NAME = "ActivityIndicator" + def __init__( self, id: str | None = None, @@ -22,8 +24,6 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.ActivityIndicator(interface=self) - if running: self.start() diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 9c00de2cac..efc1250f01 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -1,7 +1,7 @@ from __future__ import annotations from builtins import id as identifier -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, TypeVar from travertino.declaration import BaseStyle from travertino.node import Node @@ -17,6 +17,8 @@ class Widget(Node): + _IMPL_NAME = "Widget" + _MIN_WIDTH = 100 _MIN_HEIGHT = 100 @@ -34,16 +36,31 @@ def __init__( will be applied to the widget. """ super().__init__( - style=style if style else Pack(), - applicator=TogaApplicator(self), + style=style if style is not None else Pack(), + applicator=None, ) self._id = str(id if id else identifier(self)) self._window: Window | None = None self._app: App | None = None - self._impl: Any = None self.factory = get_platform_factory() + self._impl = getattr(self.factory, self._IMPL_NAME)(interface=self) + + self.applicator = TogaApplicator() + + ############################################## + # Backwards compatibility for Travertino 0.3.0 + ############################################## + + if not hasattr(self.applicator, "node"): + self.applicator.node = self + self.style._applicator = self.applicator + self.style.reapply() + + ############################# + # End backwards compatibility + ############################# def __repr__(self) -> str: return f"<{self.__class__.__name__}:0x{identifier(self):x}>" diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index 5f84160fb1..96989eec18 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -6,6 +6,8 @@ class Box(Widget): + _IMPL_NAME = "Box" + _MIN_WIDTH = 0 _MIN_HEIGHT = 0 @@ -24,9 +26,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of a Box - self._impl = self.factory.Box(interface=self) - # Children need to be added *after* the impl has been created. self._children: list[Widget] = [] if children is not None: diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index c6ab49b7b1..c491e5af5a 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -21,6 +21,8 @@ def __call__(self, widget: Button, /, **kwargs: Any) -> object: class Button(Widget): + _IMPL_NAME = "Button" + def __init__( self, text: str | None = None, @@ -44,9 +46,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of a Button - self._impl = self.factory.Button(interface=self) - # Set a dummy handler before installing the actual on_press, because we do not want # on_press triggered by the initial value being set self.on_press = None diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 8adf19c1aa..9cab38bacd 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1204,6 +1204,8 @@ def __call__( class Canvas(Widget): + _IMPL_NAME = "Canvas" + _MIN_WIDTH = 0 _MIN_HEIGHT = 0 @@ -1241,9 +1243,6 @@ def __init__( self._context = Context(canvas=self) - # Create a platform specific implementation of Canvas - self._impl = self.factory.Canvas(interface=self) - # Set all the properties self.on_resize = on_resize self.on_press = on_press diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 7408f38ae2..5ce62e61f1 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -27,6 +27,8 @@ def __call__(self, widget: DateInput, /, **kwargs: Any) -> object: class DateInput(Widget): + _IMPL_NAME = "DateInput" + _MIN_WIDTH = 200 def __init__( @@ -51,9 +53,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of a DateInput - self._impl = self.factory.DateInput(interface=self) - self.on_change = None self.min = min self.max = max diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index a5ec7937e0..b25bf7d1ee 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -52,6 +52,8 @@ def __call__(self, widget: DetailedList, /, **kwargs: Any) -> object: class DetailedList(Widget): + _IMPL_NAME = "DetailedList" + def __init__( self, id: str | None = None, @@ -85,6 +87,15 @@ def __init__( :param on_refresh: Initial :any:`on_refresh` handler. :param on_delete: **DEPRECATED**; use ``on_primary_action``. """ + # Prime the attributes and handlers that need to exist when the widget is created. + self._accessors = accessors + self._missing_value = missing_value + self._primary_action = primary_action + self._secondary_action = secondary_action + self.on_select = None + + self._data: SourceT | ListSource = None + super().__init__(id=id, style=style) ###################################################################### @@ -103,17 +114,6 @@ def __init__( # End backwards compatibility. ###################################################################### - # Prime the attributes and handlers that need to exist when the widget is created. - self._accessors = accessors - self._missing_value = missing_value - self._primary_action = primary_action - self._secondary_action = secondary_action - self.on_select = None - - self._data: SourceT | ListSource = None - - self._impl = self.factory.DetailedList(interface=self) - self.data = data self.on_primary_action = on_primary_action self.on_secondary_action = on_secondary_action diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index 4b7edf62c1..f9f65243d2 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -8,6 +8,8 @@ class Divider(Widget): + _IMPL_NAME = "Divider" + HORIZONTAL = Direction.HORIZONTAL VERTICAL = Direction.VERTICAL @@ -29,8 +31,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of a Divider - self._impl = self.factory.Divider(interface=self) self.direction = direction @property diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 663b3d1071..b65e0c6e66 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -68,6 +68,8 @@ def rehint_imageview( class ImageView(Widget): + _IMPL_NAME = "ImageView" + def __init__( self, image: ImageContentT | None = None, @@ -83,10 +85,11 @@ def __init__( :param style: A style object. If no style is provided, a default style will be applied to the widget. """ - super().__init__(id=id, style=style) # Prime the image attribute self._image = None - self._impl = self.factory.ImageView(interface=self) + + super().__init__(id=id, style=style) + self.image = image @property diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 83fb1ba7a6..273f95ca68 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -4,6 +4,8 @@ class Label(Widget): + _IMPL_NAME = "Label" + def __init__( self, text: str, @@ -19,9 +21,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of a Label - self._impl = self.factory.Label(interface=self) - self.text = text def focus(self) -> None: diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index dd69651b07..90426820c4 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -10,6 +10,8 @@ class MapPin: + _IMPL_NAME = "MapPin" + def __init__( self, location: toga.LatLng | tuple[float, float], @@ -131,6 +133,8 @@ def __call__(self, widget: MapView, /, *, pin: MapPin, **kwargs: Any) -> object: class MapView(Widget): + _IMPL_NAME = "MapView" + def __init__( self, id: str | None = None, @@ -155,8 +159,6 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl: Any = self.factory.MapView(interface=self) - self._pins = MapPinSet(self, pins) if location: diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index d987725c35..2beae8b52a 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -18,6 +18,8 @@ def __call__(self, widget: MultilineTextInput, /, **kwargs: Any) -> object: class MultilineTextInput(Widget): + _IMPL_NAME = "MultilineTextInput" + def __init__( self, id: str | None = None, @@ -42,9 +44,6 @@ def __init__( super().__init__(id=id, style=style) - # Create a platform specific implementation of a MultilineTextInput - self._impl = self.factory.MultilineTextInput(interface=self) - # 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 diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 8a9907fe67..999397134a 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -82,6 +82,8 @@ def __call__(self, widget: NumberInput, /, **kwargs: Any) -> object: class NumberInput(Widget): + _IMPL_NAME = "NumberInput" + def __init__( self, id: str | None = None, @@ -147,7 +149,6 @@ def __init__( self._max: Decimal | None = None self.on_change = None - self._impl = self.factory.NumberInput(interface=self) self.readonly = readonly self.step = step diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 8716ebb7c6..98ea4f8620 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -376,6 +376,8 @@ def insert( class OptionContainer(Widget): + _IMPL_NAME = "OptionContainer" + def __init__( self, id: str | None = None, @@ -396,8 +398,6 @@ def __init__( self._content = OptionList(self) self.on_select = None - self._impl = self.factory.OptionContainer(interface=self) - if content is not None: for item in content: if isinstance(item, OptionItem): diff --git a/core/src/toga/widgets/passwordinput.py b/core/src/toga/widgets/passwordinput.py index 48548b4935..3c2fd489a8 100644 --- a/core/src/toga/widgets/passwordinput.py +++ b/core/src/toga/widgets/passwordinput.py @@ -6,5 +6,4 @@ class PasswordInput(TextInput): """Create a new password input widget.""" - def _create(self) -> None: - self._impl = self.factory.PasswordInput(interface=self) + _IMPL_NAME = "PasswordInput" diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index 49f2e67575..417b744722 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -6,6 +6,8 @@ class ProgressBar(Widget): + _IMPL_NAME = "ProgressBar" + _MIN_WIDTH = 100 def __init__( @@ -32,8 +34,6 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.ProgressBar(interface=self) - self.max = max self.value = value diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 030cbc1bf9..77e30d392c 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -21,6 +21,8 @@ def __call__(self, widget: ScrollContainer, /, **kwargs: Any) -> object: class ScrollContainer(Widget): + _IMPL_NAME = "ScrollContainer" + def __init__( self, id: str | None = None, @@ -45,9 +47,6 @@ def __init__( self._content: Widget | None = None self.on_scroll = None - # Create a platform specific implementation of a Scroll Container - self._impl = self.factory.ScrollContainer(interface=self) - # Set all attributes self.vertical = vertical self.horizontal = horizontal diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 9b9fb5d565..a344c25089 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -23,6 +23,8 @@ def __call__(self, widget: Selection, /, **kwargs: Any) -> object: class Selection(Widget): + _IMPL_NAME = "Selection" + def __init__( self, id: str | None = None, @@ -69,7 +71,6 @@ def __init__( self._items: SourceT | ListSource self.on_change = None # needed for _impl initialization - self._impl = self.factory.Selection(interface=self) self._accessor = accessor self.items = items diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index efddce2312..a28bebb351 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -39,6 +39,8 @@ def __call__(self, widget: Slider, /, **kwargs: Any) -> object: class Slider(Widget): + _IMPL_NAME = "Slider" + def __init__( self, id: str | None = None, @@ -72,7 +74,6 @@ def __init__( :any:`range` of the slider. Defaults to ``(0, 1)``. """ super().__init__(id=id, style=style) - self._impl = self.factory.Slider(interface=self) ###################################################################### # 2023-06: Backwards compatibility diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 5d76f2a24a..7fa73f8ae2 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -20,6 +20,8 @@ class SplitContainer(Widget): + _IMPL_NAME = "SplitContainer" + HORIZONTAL = Direction.HORIZONTAL VERTICAL = Direction.VERTICAL @@ -45,9 +47,6 @@ def __init__( super().__init__(id=id, style=style) self._content: list[SplitContainerContentT] = [None, None] - # Create a platform specific implementation of a SplitContainer - self._impl = self.factory.SplitContainer(interface=self) - if content: self.content = content self.direction = direction diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 17c0dfb354..aa69729f68 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -18,6 +18,8 @@ def __call__(self, widget: Switch, /, **kwargs: Any) -> object: class Switch(Widget): + _IMPL_NAME = "Switch" + def __init__( self, text: str, @@ -41,8 +43,6 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.Switch(interface=self) - self.text = text # Set a dummy handler before installing the actual on_change, because we do not want diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index d0e991e0fb..07eb02cce8 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -34,6 +34,8 @@ def __call__(self, widget: Table, /, row: Any, **kwargs: Any) -> object: class Table(Widget): + _IMPL_NAME = "Table" + def __init__( self, headings: Iterable[str] | None = None, @@ -78,8 +80,6 @@ def __init__( defined. :param on_double_click: **DEPRECATED**; use :attr:`on_activate`. """ - super().__init__(id=id, style=style) - ###################################################################### # 2023-06: Backwards compatibility ###################################################################### @@ -119,7 +119,8 @@ def __init__( self.on_activate = None self._data = None - self._impl = self.factory.Table(interface=self) + super().__init__(id=id, style=style) + self.data = data self.on_select = on_select diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index cea1d1e44a..31199acb9d 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -48,6 +48,8 @@ def __call__(self, widget: TextInput, /, **kwargs: Any) -> object: class TextInput(Widget): """Create a new single-line text input widget.""" + _IMPL_NAME = "TextInput" + def __init__( self, id: str | None = None, @@ -81,9 +83,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of the widget - self._create() - self.placeholder = placeholder self.readonly = readonly @@ -103,9 +102,6 @@ def __init__( self.on_lose_focus = on_lose_focus self.on_gain_focus = on_gain_focus - def _create(self) -> None: - self._impl = self.factory.TextInput(interface=self) - @property def readonly(self) -> bool: """Can the value of the widget be modified by the user? diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 0a23987bb0..5cd4f36eb7 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -20,6 +20,8 @@ def __call__(self, widget: TimeInput, /, **kwargs: Any) -> object: class TimeInput(Widget): + _IMPL_NAME = "TimeInput" + def __init__( self, id: str | None = None, @@ -42,9 +44,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of a TimeInput - self._impl = self.factory.TimeInput(interface=self) - self.on_change = None self.min = min self.max = max diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 4c2b2c616e..62fb0db66c 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -34,6 +34,8 @@ def __call__(self, widget: Tree, /, **kwargs: Any) -> object: class Tree(Widget): + _IMPL_NAME = "Tree" + def __init__( self, headings: Iterable[str] | None = None, @@ -78,8 +80,6 @@ def __init__( defined. :param on_double_click: **DEPRECATED**; use :attr:`on_activate`. """ - super().__init__(id=id, style=style) - ###################################################################### # 2023-06: Backwards compatibility ###################################################################### @@ -117,7 +117,8 @@ def __init__( self.on_activate = None self._data = None - self._impl = self.factory.Tree(interface=self) + super().__init__(id=id, style=style) + self.data = data self.on_select = on_select diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 740d830af2..b381009649 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -22,6 +22,8 @@ def __call__(self, widget: WebView, /, **kwargs: Any) -> object: class WebView(Widget): + _IMPL_NAME = "WebView" + def __init__( self, id: str | None = None, @@ -44,7 +46,6 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.WebView(interface=self) self.user_agent = user_agent # Set the load handler before loading the first URL. diff --git a/core/tests/app/test_widget_registry.py b/core/tests/app/test_widget_registry.py index a5a25d96f8..44c6729811 100644 --- a/core/tests/app/test_widget_registry.py +++ b/core/tests/app/test_widget_registry.py @@ -13,7 +13,6 @@ def widget_registry(): class ExampleWidget(toga.Widget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) def __repr__(self): return f"Widget(id={self.id!r})" diff --git a/core/tests/style/pack/utils.py b/core/tests/style/pack/utils.py index 1c46b41822..6eec7b3ca0 100644 --- a/core/tests/style/pack/utils.py +++ b/core/tests/style/pack/utils.py @@ -7,9 +7,23 @@ class ExampleNode(Node): def __init__(self, name, style, size=None, children=None): - super().__init__( - style=style, children=children, applicator=TogaApplicator(self) - ) + self._impl = Mock() + self._children = None + + super().__init__(style=style, children=children, applicator=TogaApplicator()) + + ############################################## + # Backwards compatibility for Travertino 0.3.0 + ############################################## + + if not hasattr(self.applicator, "node"): + self.applicator.node = self + self.style._applicator = self.applicator + self.style.reapply() + + ############################# + # End backwards compatibility + ############################# self.name = name self._impl = Mock() diff --git a/core/tests/style/test_applicator.py b/core/tests/style/test_applicator.py index b8f2e4bb9b..308d0ed602 100644 --- a/core/tests/style/test_applicator.py +++ b/core/tests/style/test_applicator.py @@ -3,6 +3,7 @@ import toga from toga.colors import REBECCAPURPLE from toga.fonts import FANTASY +from toga.style import TogaApplicator from toga.style.pack import HIDDEN, RIGHT, VISIBLE from toga_dummy.utils import assert_action_performed_with @@ -21,7 +22,6 @@ def __init__(self, *args, **kwargs): class ExampleLeafWidget(toga.Widget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) @pytest.fixture @@ -154,3 +154,8 @@ def test_set_background_color(child, widget): widget.applicator.set_background_color(REBECCAPURPLE) assert_action_performed_with(widget, "set background color", color=REBECCAPURPLE) + + +def test_deprecated_widget_argument(widget): + with pytest.warns(DeprecationWarning): + TogaApplicator(widget) diff --git a/core/tests/widgets/test_base.py b/core/tests/widgets/test_base.py index fb71b92f53..1bb81d47a9 100644 --- a/core/tests/widgets/test_base.py +++ b/core/tests/widgets/test_base.py @@ -1,3 +1,5 @@ +from unittest.mock import Mock + import pytest import toga @@ -17,7 +19,6 @@ class ExampleWidget(toga.Widget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) self._children = [] @@ -26,7 +27,6 @@ def __init__(self, *args, **kwargs): class ExampleLeafWidget(toga.Widget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) @pytest.fixture @@ -911,6 +911,7 @@ def test_remove_from_non_parent(widget): other = ExampleWidget(id="other") child = ExampleLeafWidget(id="child_id") other.add(child) + EventLog.reset() assert widget.children == [] assert other.children == [child] @@ -1237,3 +1238,14 @@ def test_tab_index(widget): assert widget.tab_index == 8 assert attribute_value(widget, "tab_index") == tab_index + + +def test_one_reapply_during_init(): + # The style's reapply() method should be called exactly once during widget + # initialization. + + class MockedPack(Pack): + reapply = Mock() + + ExampleWidget(style=MockedPack()) + MockedPack.reapply.assert_called_once() diff --git a/core/tests/window/test_filtered_widget_registry.py b/core/tests/window/test_filtered_widget_registry.py index 7cdb6662c8..730ea87b30 100644 --- a/core/tests/window/test_filtered_widget_registry.py +++ b/core/tests/window/test_filtered_widget_registry.py @@ -7,7 +7,6 @@ class ExampleWidget(toga.Widget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) def __repr__(self): return f"Widget(id={self.id!r})" diff --git a/dummy/src/toga_dummy/widgets/base.py b/dummy/src/toga_dummy/widgets/base.py index 93743330f6..186f3fb9a3 100644 --- a/dummy/src/toga_dummy/widgets/base.py +++ b/dummy/src/toga_dummy/widgets/base.py @@ -7,7 +7,6 @@ class Widget(LoggedObject): def __init__(self, interface): super().__init__() self.interface = interface - self.interface._impl = self self.container = None self.create() diff --git a/gtk/src/toga_gtk/widgets/base.py b/gtk/src/toga_gtk/widgets/base.py index d397d25eeb..cdd9b63ddf 100644 --- a/gtk/src/toga_gtk/widgets/base.py +++ b/gtk/src/toga_gtk/widgets/base.py @@ -9,7 +9,6 @@ class Widget: def __init__(self, interface): super().__init__() self.interface = interface - self.interface._impl = self self._container = None self.native = None self.style_providers = {} @@ -24,9 +23,6 @@ def __init__(self, interface): self.native.set_name(f"toga-{self.interface.id}") self.native.get_style_context().add_class("toga") - # Ensure initial styles are applied. - self.interface.style.reapply() - @abstractmethod def create(self): ... diff --git a/iOS/src/toga_iOS/widgets/base.py b/iOS/src/toga_iOS/widgets/base.py index 59cb4f241b..4692a08286 100644 --- a/iOS/src/toga_iOS/widgets/base.py +++ b/iOS/src/toga_iOS/widgets/base.py @@ -9,12 +9,10 @@ class Widget: def __init__(self, interface): super().__init__() self.interface = interface - self.interface._impl = self self._container = None self.constraints = None self.native = None self.create() - self.interface.style.reapply() @abstractmethod def create(self): ... diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index ce10ebad3e..dd2f0a6693 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -32,7 +32,6 @@ def scale_out_vertical(self, value): class Widget(Scalable): def __init__(self, interface): self.interface = interface - self.interface._impl = self self.container = None self.create() diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 1f721aade0..e7c9c565d1 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -4,7 +4,6 @@ class Widget: def __init__(self, interface): self.interface = interface - self.interface._impl = self self._container = None self.create() diff --git a/winforms/src/toga_winforms/widgets/base.py b/winforms/src/toga_winforms/widgets/base.py index 123c98f257..0c0c8b6fee 100644 --- a/winforms/src/toga_winforms/widgets/base.py +++ b/winforms/src/toga_winforms/widgets/base.py @@ -44,13 +44,11 @@ class Widget(ABC, Scalable): def __init__(self, interface): self.interface = interface - self.interface._impl = self self._container = None self.native = None self.create() self.init_scale(self.native) - self.interface.style.reapply() @abstractmethod def create(self): ... From 1e9160bdde81e9d42c0fdf18e4c276215545fcab Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Tue, 5 Nov 2024 01:44:34 -0500 Subject: [PATCH 02/15] Moved and renamed changenote --- core/2937.misc.rst => changes/2942.misc.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/2937.misc.rst => changes/2942.misc.rst (100%) diff --git a/core/2937.misc.rst b/changes/2942.misc.rst similarity index 100% rename from core/2937.misc.rst rename to changes/2942.misc.rst From 8b27cc19ceb7ba654f9e25b4fee219812ad5bd5d Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Tue, 5 Nov 2024 01:52:18 -0500 Subject: [PATCH 03/15] Exclude branch from coverage --- core/src/toga/widgets/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index efc1250f01..a75040886f 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -57,6 +57,8 @@ def __init__( self.applicator.node = self self.style._applicator = self.applicator self.style.reapply() + else: # pragma: no cover + pass ############################# # End backwards compatibility From 3588165a5efd21f40c7c291017300f2a60973d34 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 7 Nov 2024 21:24:23 -0500 Subject: [PATCH 04/15] comment backwards compatibility; alias widget to node; put _impl creation back in subclass inits --- cocoa/src/toga_cocoa/widgets/button.py | 2 +- core/src/toga/style/applicator.py | 50 +++++++++++++------ core/src/toga/widgets/activityindicator.py | 7 ++- core/src/toga/widgets/base.py | 16 +++--- core/src/toga/widgets/box.py | 7 ++- core/src/toga/widgets/button.py | 6 ++- core/src/toga/widgets/canvas.py | 5 +- core/src/toga/widgets/dateinput.py | 4 ++ core/src/toga/widgets/detailedlist.py | 6 ++- core/src/toga/widgets/divider.py | 6 ++- core/src/toga/widgets/imageview.py | 6 ++- core/src/toga/widgets/label.py | 7 ++- core/src/toga/widgets/mapview.py | 6 ++- core/src/toga/widgets/multilinetextinput.py | 5 +- core/src/toga/widgets/numberinput.py | 6 ++- core/src/toga/widgets/optioncontainer.py | 5 +- core/src/toga/widgets/passwordinput.py | 3 +- core/src/toga/widgets/progressbar.py | 7 ++- core/src/toga/widgets/scrollcontainer.py | 6 ++- core/src/toga/widgets/selection.py | 6 ++- core/src/toga/widgets/slider.py | 6 ++- core/src/toga/widgets/splitcontainer.py | 6 ++- core/src/toga/widgets/switch.py | 6 ++- core/src/toga/widgets/table.py | 6 ++- core/src/toga/widgets/textinput.py | 12 ++++- core/src/toga/widgets/timeinput.py | 6 ++- core/src/toga/widgets/tree.py | 6 ++- core/src/toga/widgets/webview.py | 6 ++- core/tests/app/test_widget_registry.py | 36 ++++++------- core/tests/style/test_applicator.py | 26 ++++------ core/tests/utils.py | 30 +++++++++++ core/tests/widgets/test_base.py | 15 +----- core/tests/window/__init__.py | 0 .../window/test_filtered_widget_registry.py | 17 ++----- dummy/src/toga_dummy/utils.py | 9 +++- 35 files changed, 217 insertions(+), 136 deletions(-) create mode 100644 core/tests/utils.py create mode 100644 core/tests/window/__init__.py diff --git a/cocoa/src/toga_cocoa/widgets/button.py b/cocoa/src/toga_cocoa/widgets/button.py index 1ef61bcefd..bd42e6ebc7 100644 --- a/cocoa/src/toga_cocoa/widgets/button.py +++ b/cocoa/src/toga_cocoa/widgets/button.py @@ -32,7 +32,7 @@ def create(self): self._icon = None self.native.buttonType = NSMomentaryPushInButton - self._set_button_style() + # self._set_button_style() self.native.target = self.native self.native.action = SEL("onPress:") diff --git a/core/src/toga/style/applicator.py b/core/src/toga/style/applicator.py index 4545d79d8e..0c28b74982 100644 --- a/core/src/toga/style/applicator.py +++ b/core/src/toga/style/applicator.py @@ -1,4 +1,10 @@ +from __future__ import annotations + import warnings +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from toga.widgets.base import Widget # Make sure deprecation warnings are shown by default warnings.filterwarnings("default", category=DeprecationWarning) @@ -16,27 +22,39 @@ def __init__(self, widget: None = None): stacklevel=2, ) + @property + def widget(self) -> Widget: + """The widget to which this applicator is assigned. + + Syntactic sugar over the node attribute set by Travertino. + """ + return self.node + + @widget.setter + def widget(self, widget: Widget): + self.node = widget + def refresh(self) -> None: - # print("RE-EVALUATE LAYOUT", self.node) - self.node.refresh() + # print("RE-EVALUATE LAYOUT", self.widget) + self.widget.refresh() def set_bounds(self) -> None: - # print(" APPLY LAYOUT", self.node, self.node.layout) - self.node._impl.set_bounds( - self.node.layout.absolute_content_left, - self.node.layout.absolute_content_top, - self.node.layout.content_width, - self.node.layout.content_height, + # print(" APPLY LAYOUT", self.widget, self.widget.layout) + self.widget._impl.set_bounds( + self.widget.layout.absolute_content_left, + self.widget.layout.absolute_content_top, + self.widget.layout.content_width, + self.widget.layout.content_height, ) - for child in self.node.children: + for child in self.widget.children: child.applicator.set_bounds() def set_text_alignment(self, alignment: str) -> None: - self.node._impl.set_alignment(alignment) + self.widget._impl.set_alignment(alignment) def set_hidden(self, hidden: bool) -> None: - self.node._impl.set_hidden(hidden) - for child in self.node.children: + self.widget._impl.set_hidden(hidden) + for child in self.widget.children: # If the parent is hidden, then so are all children. However, if the # parent is visible, then the child's explicit visibility style is # taken into account. This visibility cascades into any @@ -53,11 +71,11 @@ def set_hidden(self, hidden: bool) -> None: def set_font(self, font: object) -> None: # Changing the font of a widget can make the widget change size, # which in turn means we need to do a re-layout - self.node._impl.set_font(font) - self.node.refresh() + self.widget._impl.set_font(font) + self.widget.refresh() def set_color(self, color: object) -> None: - self.node._impl.set_color(color) + self.widget._impl.set_color(color) def set_background_color(self, color: object) -> None: - self.node._impl.set_background_color(color) + self.widget._impl.set_background_color(color) diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index 7bc731f7fb..c18039164b 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -2,12 +2,12 @@ from typing import Literal +from toga.platform import get_platform_factory + from .base import StyleT, Widget class ActivityIndicator(Widget): - _IMPL_NAME = "ActivityIndicator" - def __init__( self, id: str | None = None, @@ -22,6 +22,9 @@ def __init__( :param running: Describes whether the indicator is running at the time it is created. """ + self.factory = get_platform_factory() + self._impl = self.factory.ActivityIndicator(interface=self) + super().__init__(id=id, style=style) if running: diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index a75040886f..99b2a503a4 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -6,7 +6,6 @@ from travertino.declaration import BaseStyle from travertino.node import Node -from toga.platform import get_platform_factory from toga.style import Pack, TogaApplicator if TYPE_CHECKING: @@ -44,21 +43,24 @@ def __init__( self._window: Window | None = None self._app: App | None = None - self.factory = get_platform_factory() - self._impl = getattr(self.factory, self._IMPL_NAME)(interface=self) - self.applicator = TogaApplicator() ############################################## # Backwards compatibility for Travertino 0.3.0 ############################################## - if not hasattr(self.applicator, "node"): + # The below if block will execute when using Travertino 0.3.0. For future + # versions of Travertino, these assignments (and the reapply) will already have + # been handled "automatically" by assigning the applicator above; in that case, + # we want to avoid doing a second, redundant style reapplication. + + # This whole section can be removed as soon as there's a newer version of + # Travertino to set as Toga's minimum requirement. + + if not hasattr(self.applicator, "node"): # pragma: no cover self.applicator.node = self self.style._applicator = self.applicator self.style.reapply() - else: # pragma: no cover - pass ############################# # End backwards compatibility diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index 96989eec18..d8c4606a3c 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -2,12 +2,12 @@ from collections.abc import Iterable +from toga.platform import get_platform_factory + from .base import StyleT, Widget class Box(Widget): - _IMPL_NAME = "Box" - _MIN_WIDTH = 0 _MIN_HEIGHT = 0 @@ -24,6 +24,9 @@ def __init__( will be applied to the widget. :param children: An optional list of children for to add to the Box. """ + self.factory = get_platform_factory() + self._impl = self.factory.Box(interface=self) + super().__init__(id=id, style=style) # Children need to be added *after* the impl has been created. diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index c491e5af5a..5b263edbd8 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -4,6 +4,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -21,8 +22,6 @@ def __call__(self, widget: Button, /, **kwargs: Any) -> object: class Button(Widget): - _IMPL_NAME = "Button" - def __init__( self, text: str | None = None, @@ -44,6 +43,9 @@ def __init__( :param enabled: Is the button enabled (i.e., can it be pressed?). Optional; by default, buttons are created in an enabled state. """ + self.factory = get_platform_factory() + self._impl = self.factory.Button(interface=self) + super().__init__(id=id, style=style) # Set a dummy handler before installing the actual on_press, because we do not want diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 9cab38bacd..dd6b382dbe 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -25,6 +25,7 @@ Font, ) from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -1204,8 +1205,6 @@ def __call__( class Canvas(Widget): - _IMPL_NAME = "Canvas" - _MIN_WIDTH = 0 _MIN_HEIGHT = 0 @@ -1238,6 +1237,8 @@ def __init__( :param on_alt_release: Initial :any:`on_alt_release` handler. :param on_alt_drag: Initial :any:`on_alt_drag` handler. """ + self.factory = get_platform_factory() + self._impl = self.factory.Canvas(interface=self) super().__init__(id=id, style=style) diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 5ce62e61f1..c1fac6fb2a 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -6,6 +6,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -51,6 +52,9 @@ def __init__( :param max: The latest date (inclusive) that can be selected. :param on_change: A handler that will be invoked when the value changes. """ + self.factory = get_platform_factory() + self._impl = self.factory.DateInput(interface=self) + super().__init__(id=id, style=style) self.on_change = None diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index b25bf7d1ee..7a8a3085bd 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -6,6 +6,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from toga.sources import ListSource, Row, Source from .base import StyleT, Widget @@ -52,8 +53,6 @@ def __call__(self, widget: DetailedList, /, **kwargs: Any) -> object: class DetailedList(Widget): - _IMPL_NAME = "DetailedList" - def __init__( self, id: str | None = None, @@ -96,6 +95,9 @@ def __init__( self._data: SourceT | ListSource = None + self.factory = get_platform_factory() + self._impl = self.factory.DetailedList(interface=self) + super().__init__(id=id, style=style) ###################################################################### diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index f9f65243d2..d72ede7364 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -3,13 +3,12 @@ from typing import Literal from toga.constants import Direction +from toga.platform import get_platform_factory from .base import StyleT, Widget class Divider(Widget): - _IMPL_NAME = "Divider" - HORIZONTAL = Direction.HORIZONTAL VERTICAL = Direction.VERTICAL @@ -29,6 +28,9 @@ def __init__( :attr:`~toga.constants.Direction.VERTICAL`; defaults to :attr:`~toga.constants.Direction.HORIZONTAL` """ + self.factory = get_platform_factory() + self._impl = self.factory.Divider(interface=self) + super().__init__(id=id, style=style) self.direction = direction diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index b65e0c6e66..0b52bc341c 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -5,6 +5,7 @@ from travertino.size import at_least import toga +from toga.platform import get_platform_factory from toga.style.pack import NONE from toga.widgets.base import StyleT, Widget @@ -68,8 +69,6 @@ def rehint_imageview( class ImageView(Widget): - _IMPL_NAME = "ImageView" - def __init__( self, image: ImageContentT | None = None, @@ -88,6 +87,9 @@ def __init__( # Prime the image attribute self._image = None + self.factory = get_platform_factory() + self._impl = self.factory.ImageView(interface=self) + super().__init__(id=id, style=style) self.image = image diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 273f95ca68..79cdcba2c7 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -1,11 +1,11 @@ from __future__ import annotations +from toga.platform import get_platform_factory + from .base import StyleT, Widget class Label(Widget): - _IMPL_NAME = "Label" - def __init__( self, text: str, @@ -19,6 +19,9 @@ def __init__( :param style: A style object. If no style is provided, a default style will be applied to the widget. """ + self.factory = get_platform_factory() + self._impl = self.factory.Label(interface=self) + super().__init__(id=id, style=style) self.text = text diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 90426820c4..6616e80627 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -5,6 +5,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -133,8 +134,6 @@ def __call__(self, widget: MapView, /, *, pin: MapPin, **kwargs: Any) -> object: class MapView(Widget): - _IMPL_NAME = "MapView" - def __init__( self, id: str | None = None, @@ -157,6 +156,9 @@ def __init__( :param on_select: A handler that will be invoked when the user selects a map pin. """ + self.factory = get_platform_factory() + self._impl = self.factory.MapView(interface=self) + super().__init__(id=id, style=style) self._pins = MapPinSet(self, pins) diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 2beae8b52a..7e432bafa3 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -4,6 +4,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -18,8 +19,6 @@ def __call__(self, widget: MultilineTextInput, /, **kwargs: Any) -> object: class MultilineTextInput(Widget): - _IMPL_NAME = "MultilineTextInput" - def __init__( self, id: str | None = None, @@ -41,6 +40,8 @@ def __init__( :param on_change: A handler that will be invoked when the value of the widget changes. """ + self.factory = get_platform_factory() + self._impl = self.factory.MultilineTextInput(interface=self) super().__init__(id=id, style=style) diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 999397134a..49d0e1c2f8 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -8,6 +8,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -82,8 +83,6 @@ def __call__(self, widget: NumberInput, /, **kwargs: Any) -> object: class NumberInput(Widget): - _IMPL_NAME = "NumberInput" - def __init__( self, id: str | None = None, @@ -115,6 +114,9 @@ def __init__( :param min_value: **DEPRECATED**; alias of ``min``. :param max_value: **DEPRECATED**; alias of ``max``. """ + self.factory = get_platform_factory() + self._impl = self.factory.NumberInput(interface=self) + super().__init__(id=id, style=style) ###################################################################### diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 98ea4f8620..1441ab6786 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -376,8 +376,6 @@ def insert( class OptionContainer(Widget): - _IMPL_NAME = "OptionContainer" - def __init__( self, id: str | None = None, @@ -394,6 +392,9 @@ def __init__( ` to display in the OptionContainer. :param on_select: Initial :any:`on_select` handler. """ + self.factory = get_platform_factory() + self._impl = self.factory.OptionContainer(interface=self) + super().__init__(id=id, style=style) self._content = OptionList(self) self.on_select = None diff --git a/core/src/toga/widgets/passwordinput.py b/core/src/toga/widgets/passwordinput.py index 3c2fd489a8..48548b4935 100644 --- a/core/src/toga/widgets/passwordinput.py +++ b/core/src/toga/widgets/passwordinput.py @@ -6,4 +6,5 @@ class PasswordInput(TextInput): """Create a new password input widget.""" - _IMPL_NAME = "PasswordInput" + def _create(self) -> None: + self._impl = self.factory.PasswordInput(interface=self) diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index 417b744722..8c2c7462c0 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -2,12 +2,12 @@ from typing import Literal, SupportsFloat +from toga.platform import get_platform_factory + from .base import StyleT, Widget class ProgressBar(Widget): - _IMPL_NAME = "ProgressBar" - _MIN_WIDTH = 100 def __init__( @@ -32,6 +32,9 @@ def __init__( :param running: Describes whether the indicator is running at the time it is created. Default is False. """ + self.factory = get_platform_factory() + self._impl = self.factory.ProgressBar(interface=self) + super().__init__(id=id, style=style) self.max = max diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 77e30d392c..5f03dc4825 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Literal, Protocol, SupportsInt from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from toga.types import Position from .base import StyleT, Widget @@ -21,8 +22,6 @@ def __call__(self, widget: ScrollContainer, /, **kwargs: Any) -> object: class ScrollContainer(Widget): - _IMPL_NAME = "ScrollContainer" - def __init__( self, id: str | None = None, @@ -42,6 +41,9 @@ def __init__( :param on_scroll: Initial :any:`on_scroll` handler. :param content: The content to display in the scroll window. """ + self.factory = get_platform_factory() + self._impl = self.factory.ScrollContainer(interface=self) + super().__init__(id=id, style=style) self._content: Widget | None = None diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index a344c25089..2ac193c9af 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -6,6 +6,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from toga.sources import ListSource, Source from .base import StyleT, Widget @@ -23,8 +24,6 @@ def __call__(self, widget: Selection, /, **kwargs: Any) -> object: class Selection(Widget): - _IMPL_NAME = "Selection" - def __init__( self, id: str | None = None, @@ -50,6 +49,9 @@ def __init__( :param on_change: Initial :any:`on_change` handler. :param enabled: Whether the user can interact with the widget. """ + self.factory = get_platform_factory() + self._impl = self.factory.Selection(interface=self) + super().__init__(id=id, style=style) ###################################################################### diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index a28bebb351..ce7d5a0c51 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -7,6 +7,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -39,8 +40,6 @@ def __call__(self, widget: Slider, /, **kwargs: Any) -> object: class Slider(Widget): - _IMPL_NAME = "Slider" - def __init__( self, id: str | None = None, @@ -73,6 +72,9 @@ def __init__( :param range: **DEPRECATED**; use ``min`` and ``max`` instead. Initial :any:`range` of the slider. Defaults to ``(0, 1)``. """ + self.factory = get_platform_factory() + self._impl = self.factory.Slider(interface=self) + super().__init__(id=id, style=style) ###################################################################### diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 7fa73f8ae2..e3f591ed56 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -6,6 +6,7 @@ from toga.app import App from toga.constants import Direction +from toga.platform import get_platform_factory from toga.window import Window from .base import StyleT, Widget @@ -20,8 +21,6 @@ class SplitContainer(Widget): - _IMPL_NAME = "SplitContainer" - HORIZONTAL = Direction.HORIZONTAL VERTICAL = Direction.VERTICAL @@ -44,6 +43,9 @@ def __init__( :param content: Initial :any:`SplitContainer content ` of the container. Defaults to both panels being empty. """ + self.factory = get_platform_factory() + self._impl = self.factory.SplitContainer(interface=self) + super().__init__(id=id, style=style) self._content: list[SplitContainerContentT] = [None, None] diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index aa69729f68..f001e67a0b 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -4,6 +4,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -18,8 +19,6 @@ def __call__(self, widget: Switch, /, **kwargs: Any) -> object: class Switch(Widget): - _IMPL_NAME = "Switch" - def __init__( self, text: str, @@ -41,6 +40,9 @@ def __init__( :param enabled: Is the switch enabled (i.e., can it be pressed?). Optional; by default, switches are created in an enabled state. """ + self.factory = get_platform_factory() + self._impl = self.factory.Switch(interface=self) + super().__init__(id=id, style=style) self.text = text diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 07eb02cce8..4dd572c72f 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -6,6 +6,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from toga.sources import ListSource, Row, Source from toga.sources.accessors import build_accessors, to_accessor @@ -34,8 +35,6 @@ def __call__(self, widget: Table, /, row: Any, **kwargs: Any) -> object: class Table(Widget): - _IMPL_NAME = "Table" - def __init__( self, headings: Iterable[str] | None = None, @@ -119,6 +118,9 @@ def __init__( self.on_activate = None self._data = None + self.factory = get_platform_factory() + self._impl = self.factory.Table(interface=self) + super().__init__(id=id, style=style) self.data = data diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 31199acb9d..f2f15fd334 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -5,6 +5,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -48,8 +49,6 @@ def __call__(self, widget: TextInput, /, **kwargs: Any) -> object: class TextInput(Widget): """Create a new single-line text input widget.""" - _IMPL_NAME = "TextInput" - def __init__( self, id: str | None = None, @@ -81,6 +80,12 @@ def __init__( input focus. :param validators: A list of validators to run on the value of the input. """ + self.factory = get_platform_factory() + self._impl = self.factory.TextInput(interface=self) + + self.factory = get_platform_factory() + self._create() + super().__init__(id=id, style=style) self.placeholder = placeholder @@ -102,6 +107,9 @@ def __init__( self.on_lose_focus = on_lose_focus self.on_gain_focus = on_gain_focus + def _create(self) -> None: + self._impl = self.factory.TextInput(interface=self) + @property def readonly(self) -> bool: """Can the value of the widget be modified by the user? diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 5cd4f36eb7..820382894f 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -6,6 +6,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -20,8 +21,6 @@ def __call__(self, widget: TimeInput, /, **kwargs: Any) -> object: class TimeInput(Widget): - _IMPL_NAME = "TimeInput" - def __init__( self, id: str | None = None, @@ -42,6 +41,9 @@ def __init__( :param max: The latest time (inclusive) that can be selected. :param on_change: A handler that will be invoked when the value changes. """ + self.factory = get_platform_factory() + self._impl = self.factory.TimeInput(interface=self) + super().__init__(id=id, style=style) self.on_change = None diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 62fb0db66c..c25d639483 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -6,6 +6,7 @@ import toga from toga.handlers import wrapped_handler +from toga.platform import get_platform_factory from toga.sources import Node, Source, TreeSource from toga.sources.accessors import build_accessors, to_accessor from toga.style import Pack @@ -34,8 +35,6 @@ def __call__(self, widget: Tree, /, **kwargs: Any) -> object: class Tree(Widget): - _IMPL_NAME = "Tree" - def __init__( self, headings: Iterable[str] | None = None, @@ -117,6 +116,9 @@ def __init__( self.on_activate = None self._data = None + self.factory = get_platform_factory() + self._impl = self.factory.Tree(interface=self) + super().__init__(id=id, style=style) self.data = data diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index b381009649..aab90e9ccf 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -4,6 +4,7 @@ from typing import Any, Protocol from toga.handlers import AsyncResult, OnResultT, wrapped_handler +from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -22,8 +23,6 @@ def __call__(self, widget: WebView, /, **kwargs: Any) -> object: class WebView(Widget): - _IMPL_NAME = "WebView" - def __init__( self, id: str | None = None, @@ -44,6 +43,9 @@ def __init__( :param on_webview_load: A handler that will be invoked when the web view finishes loading. """ + self.factory = get_platform_factory() + self._impl = self.factory.WebView(interface=self) + super().__init__(id=id, style=style) self.user_agent = user_agent diff --git a/core/tests/app/test_widget_registry.py b/core/tests/app/test_widget_registry.py index 44c6729811..0e5d567056 100644 --- a/core/tests/app/test_widget_registry.py +++ b/core/tests/app/test_widget_registry.py @@ -1,23 +1,15 @@ import pytest -import toga from toga.app import WidgetRegistry +from ..utils import ExampleLeafWidget + @pytest.fixture def widget_registry(): return WidgetRegistry() -# Create the simplest possible widget with a concrete implementation -class ExampleWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"Widget(id={self.id!r})" - - def test_empty_registry(widget_registry): assert len(widget_registry) == 0 assert list(widget_registry) == [] @@ -27,7 +19,7 @@ def test_empty_registry(widget_registry): def test_add_widget(widget_registry): """Widgets can be added to the registry.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) assert len(widget_registry) == 1 @@ -36,7 +28,7 @@ def test_add_widget(widget_registry): assert widget_registry["widget-1"] == widget1 # Add a second widget - widget2 = ExampleWidget(id="widget-2") + widget2 = ExampleLeafWidget(id="widget-2") widget_registry._add(widget2) assert len(widget_registry) == 2 @@ -47,12 +39,12 @@ def test_add_widget(widget_registry): def test_update_widgets(widget_registry): """The registry can be bulk updated.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) - widget2 = ExampleWidget(id="widget-2") - widget3 = ExampleWidget(id="widget-3") - widget4 = ExampleWidget(id="widget-4") + widget2 = ExampleLeafWidget(id="widget-2") + widget3 = ExampleLeafWidget(id="widget-3") + widget4 = ExampleLeafWidget(id="widget-4") widget_registry._update({widget2, widget3, widget4}) assert len(widget_registry) == 4 @@ -66,8 +58,8 @@ def test_remove_widget(widget_registry): """A widget can be removed from the repository.""" "Widgets can be added to the registry" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") - widget2 = ExampleWidget(id="widget-2") + widget1 = ExampleLeafWidget(id="widget-1") + widget2 = ExampleLeafWidget(id="widget-2") widget_registry._update({widget1, widget2}) assert len(widget_registry) == 2 @@ -81,7 +73,7 @@ def test_remove_widget(widget_registry): def test_add_same_widget_twice(widget_registry): """A widget cannot be added to the same registry twice.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) assert len(widget_registry) == 1 @@ -101,12 +93,12 @@ def test_add_same_widget_twice(widget_registry): def test_add_duplicate_id(widget_registry): """A widget cannot be added to the same registry twice.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) assert len(widget_registry) == 1 - new_widget = ExampleWidget(id="widget-1") + new_widget = ExampleLeafWidget(id="widget-1") # Add the widget again; this raises an error with pytest.raises( @@ -122,7 +114,7 @@ def test_add_duplicate_id(widget_registry): def test_setitem(widget_registry): """Widgets cannot be directly assigned to the registry.""" - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") with pytest.raises( TypeError, diff --git a/core/tests/style/test_applicator.py b/core/tests/style/test_applicator.py index 308d0ed602..01a0594bf3 100644 --- a/core/tests/style/test_applicator.py +++ b/core/tests/style/test_applicator.py @@ -1,27 +1,12 @@ import pytest -import toga from toga.colors import REBECCAPURPLE from toga.fonts import FANTASY from toga.style import TogaApplicator from toga.style.pack import HIDDEN, RIGHT, VISIBLE from toga_dummy.utils import assert_action_performed_with - -# Create the simplest possible widget with a concrete implementation that will -# allow children -class ExampleWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) - self._children = [] - - -# Create the simplest possible widget with a concrete implementation that cannot -# have children. -class ExampleLeafWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) +from ..utils import ExampleLeafWidget, ExampleWidget @pytest.fixture @@ -157,5 +142,14 @@ def test_set_background_color(child, widget): def test_deprecated_widget_argument(widget): + """The widget argument to TogaApplicator is deprecated.""" with pytest.warns(DeprecationWarning): TogaApplicator(widget) + + +def test_widget_alias_to_node(widget): + """Applicator.widget is an alias to applicator.node.""" + applicator = widget.applicator + + assert applicator.widget is widget + assert applicator.widget is applicator.node diff --git a/core/tests/utils.py b/core/tests/utils.py new file mode 100644 index 0000000000..e9bd314fa3 --- /dev/null +++ b/core/tests/utils.py @@ -0,0 +1,30 @@ +import toga +from toga.platform import get_platform_factory + + +# Create the simplest possible widget with a concrete implementation that will +# allow children +class ExampleWidget(toga.Widget): + def __init__(self, *args, **kwargs): + self.factory = get_platform_factory() + self._impl = self.factory.Widget(self) + + super().__init__(*args, **kwargs) + + self._children = [] + + def __repr__(self): + return f"Widget(id={self.id!r})" + + +# Create the simplest possible widget with a concrete implementation that cannot +# have children. +class ExampleLeafWidget(toga.Widget): + def __init__(self, *args, **kwargs): + self.factory = get_platform_factory() + self._impl = self.factory.Widget(self) + + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"Widget(id={self.id!r})" diff --git a/core/tests/widgets/test_base.py b/core/tests/widgets/test_base.py index 1bb81d47a9..494995a832 100644 --- a/core/tests/widgets/test_base.py +++ b/core/tests/widgets/test_base.py @@ -13,20 +13,7 @@ attribute_value, ) - -# Create the simplest possible widget with a concrete implementation that will -# allow children -class ExampleWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._children = [] - - -# Create the simplest possible widget with a concrete implementation that cannot -# have children. -class ExampleLeafWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) +from ..utils import ExampleLeafWidget, ExampleWidget @pytest.fixture diff --git a/core/tests/window/__init__.py b/core/tests/window/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/tests/window/test_filtered_widget_registry.py b/core/tests/window/test_filtered_widget_registry.py index 730ea87b30..44be5ce324 100644 --- a/core/tests/window/test_filtered_widget_registry.py +++ b/core/tests/window/test_filtered_widget_registry.py @@ -2,14 +2,7 @@ import toga - -# Create the simplest possible widget with a concrete implementation -class ExampleWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"Widget(id={self.id!r})" +from ..utils import ExampleLeafWidget # Create a box subclass with a reproducible repr @@ -23,8 +16,8 @@ def make_window(win_id): win.content = ExampleBox( id=f"{win_id}.0", children=[ - ExampleWidget(id=f"{win_id}.1"), - ExampleWidget(id=f"{win_id}.2"), + ExampleLeafWidget(id=f"{win_id}.1"), + ExampleLeafWidget(id=f"{win_id}.2"), ], ) return win @@ -154,7 +147,7 @@ def test_reuse_id(app): # Create a widget with the magic ID. The widget still isn't in the registry, because # it's not part of a window - first = ExampleWidget(id="magic") + first = ExampleLeafWidget(id="magic") assert "magic" not in app.widgets assert "magic" not in win_1.widgets @@ -170,7 +163,7 @@ def test_reuse_id(app): # Create a second widget with the same ID. # The widget exists, but the registry is storing `first`. - second = ExampleWidget(id="magic") + second = ExampleLeafWidget(id="magic") assert "magic" in app.widgets assert "magic" in win_1.widgets diff --git a/dummy/src/toga_dummy/utils.py b/dummy/src/toga_dummy/utils.py index 2cb1ce3358..d5ab2e5f32 100644 --- a/dummy/src/toga_dummy/utils.py +++ b/dummy/src/toga_dummy/utils.py @@ -371,7 +371,14 @@ def assert_action_performed_with(_widget, _action, **test_data): found = False except AttributeError: # No raw attribute; use the provided value as-is - if data[key] != value: + try: + if data[key] != value: + found = False + ######################################################## + # Backwards compatibility for Travertino 0.3.0 + # Font.__eq__ throws an AttributeError against non-Fonts + ######################################################## + except AttributeError: found = False except KeyError: found = False From 552eef2ad9fed5fe6d4e4d161f4540410f344e3b Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 14 Nov 2024 20:21:52 -0500 Subject: [PATCH 05/15] Switched to _create method --- core/src/toga/style/applicator.py | 4 ---- core/src/toga/widgets/activityindicator.py | 7 ++++--- core/src/toga/widgets/base.py | 9 +++++++++ core/src/toga/widgets/box.py | 7 ++++--- core/src/toga/widgets/button.py | 7 ++++--- core/src/toga/widgets/canvas.py | 7 ++++--- core/src/toga/widgets/dateinput.py | 7 ++++--- core/src/toga/widgets/detailedlist.py | 7 ++++--- core/src/toga/widgets/divider.py | 7 ++++--- core/src/toga/widgets/imageview.py | 7 ++++--- core/src/toga/widgets/label.py | 7 ++++--- core/src/toga/widgets/mapview.py | 7 ++++--- core/src/toga/widgets/multilinetextinput.py | 7 ++++--- core/src/toga/widgets/numberinput.py | 7 ++++--- core/src/toga/widgets/optioncontainer.py | 7 ++++--- core/src/toga/widgets/passwordinput.py | 3 +++ core/src/toga/widgets/progressbar.py | 7 ++++--- core/src/toga/widgets/scrollcontainer.py | 7 ++++--- core/src/toga/widgets/selection.py | 7 ++++--- core/src/toga/widgets/slider.py | 9 +++++---- core/src/toga/widgets/splitcontainer.py | 7 ++++--- core/src/toga/widgets/switch.py | 7 ++++--- core/src/toga/widgets/table.py | 7 ++++--- core/src/toga/widgets/textinput.py | 7 +------ core/src/toga/widgets/timeinput.py | 7 ++++--- core/src/toga/widgets/tree.py | 7 ++++--- core/src/toga/widgets/webview.py | 7 ++++--- core/tests/utils.py | 13 ++++++------- 28 files changed, 112 insertions(+), 87 deletions(-) diff --git a/core/src/toga/style/applicator.py b/core/src/toga/style/applicator.py index 0c28b74982..43674a0d24 100644 --- a/core/src/toga/style/applicator.py +++ b/core/src/toga/style/applicator.py @@ -30,10 +30,6 @@ def widget(self) -> Widget: """ return self.node - @widget.setter - def widget(self, widget: Widget): - self.node = widget - def refresh(self) -> None: # print("RE-EVALUATE LAYOUT", self.widget) self.widget.refresh() diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index c18039164b..1fee04f06b 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -22,14 +22,15 @@ def __init__( :param running: Describes whether the indicator is running at the time it is created. """ - self.factory = get_platform_factory() - self._impl = self.factory.ActivityIndicator(interface=self) - super().__init__(id=id, style=style) if running: self.start() + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.ActivityIndicator(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 99b2a503a4..a0b046246b 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -39,6 +39,9 @@ def __init__( applicator=None, ) + # Create and assign _impl + self._create() + self._id = str(id if id else identifier(self)) self._window: Window | None = None self._app: App | None = None @@ -66,6 +69,12 @@ def __init__( # End backwards compatibility ############################# + def _create(self) -> None: + raise NotImplementedError( + "Widget must define a _create method to create and assign its " + "implementation." + ) # pragma: no cover + def __repr__(self) -> str: return f"<{self.__class__.__name__}:0x{identifier(self):x}>" diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index d8c4606a3c..0944c2ff67 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -24,9 +24,6 @@ def __init__( will be applied to the widget. :param children: An optional list of children for to add to the Box. """ - self.factory = get_platform_factory() - self._impl = self.factory.Box(interface=self) - super().__init__(id=id, style=style) # Children need to be added *after* the impl has been created. @@ -34,6 +31,10 @@ def __init__( if children is not None: self.add(*children) + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Box(interface=self) + @property def enabled(self) -> bool: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index 5b263edbd8..6f539bebec 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -43,9 +43,6 @@ def __init__( :param enabled: Is the button enabled (i.e., can it be pressed?). Optional; by default, buttons are created in an enabled state. """ - self.factory = get_platform_factory() - self._impl = self.factory.Button(interface=self) - super().__init__(id=id, style=style) # Set a dummy handler before installing the actual on_press, because we do not want @@ -64,6 +61,10 @@ def __init__( self.on_press = on_press self.enabled = enabled + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Button(interface=self) + @property def text(self) -> str: """The text displayed on the button. diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index dd6b382dbe..8f04d117de 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1237,9 +1237,6 @@ def __init__( :param on_alt_release: Initial :any:`on_alt_release` handler. :param on_alt_drag: Initial :any:`on_alt_drag` handler. """ - self.factory = get_platform_factory() - self._impl = self.factory.Canvas(interface=self) - super().__init__(id=id, style=style) self._context = Context(canvas=self) @@ -1254,6 +1251,10 @@ def __init__( self.on_alt_release = on_alt_release self.on_alt_drag = on_alt_drag + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Canvas(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index c1fac6fb2a..bc144af678 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -52,9 +52,6 @@ def __init__( :param max: The latest date (inclusive) that can be selected. :param on_change: A handler that will be invoked when the value changes. """ - self.factory = get_platform_factory() - self._impl = self.factory.DateInput(interface=self) - super().__init__(id=id, style=style) self.on_change = None @@ -64,6 +61,10 @@ def __init__( self.value = value self.on_change = on_change + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.DateInput(interface=self) + @property def value(self) -> datetime.date: """The currently selected date. A value of ``None`` will be converted into diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 7a8a3085bd..7032479c2d 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -95,9 +95,6 @@ def __init__( self._data: SourceT | ListSource = None - self.factory = get_platform_factory() - self._impl = self.factory.DetailedList(interface=self) - super().__init__(id=id, style=style) ###################################################################### @@ -122,6 +119,10 @@ def __init__( self.on_refresh = on_refresh self.on_select = on_select + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.DetailedList(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index d72ede7364..3094f60673 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -28,13 +28,14 @@ def __init__( :attr:`~toga.constants.Direction.VERTICAL`; defaults to :attr:`~toga.constants.Direction.HORIZONTAL` """ - self.factory = get_platform_factory() - self._impl = self.factory.Divider(interface=self) - super().__init__(id=id, style=style) self.direction = direction + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Divider(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 0b52bc341c..2ad4c1b181 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -87,13 +87,14 @@ def __init__( # Prime the image attribute self._image = None - self.factory = get_platform_factory() - self._impl = self.factory.ImageView(interface=self) - super().__init__(id=id, style=style) self.image = image + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.ImageView(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 79cdcba2c7..1d7ef6243d 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -19,13 +19,14 @@ def __init__( :param style: A style object. If no style is provided, a default style will be applied to the widget. """ - self.factory = get_platform_factory() - self._impl = self.factory.Label(interface=self) - super().__init__(id=id, style=style) self.text = text + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Label(interface=self) + def focus(self) -> None: """No-op; Label cannot accept input focus.""" pass diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 6616e80627..905381c6d3 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -156,9 +156,6 @@ def __init__( :param on_select: A handler that will be invoked when the user selects a map pin. """ - self.factory = get_platform_factory() - self._impl = self.factory.MapView(interface=self) - super().__init__(id=id, style=style) self._pins = MapPinSet(self, pins) @@ -173,6 +170,10 @@ def __init__( self.on_select = on_select + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.MapView(interface=self) + @property def location(self) -> toga.LatLng: """The latitude/longitude where the map is centered. diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 7e432bafa3..4e1b141e0e 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -40,9 +40,6 @@ def __init__( :param on_change: A handler that will be invoked when the value of the widget changes. """ - self.factory = get_platform_factory() - self._impl = self.factory.MultilineTextInput(interface=self) - super().__init__(id=id, style=style) # Set a dummy handler before installing the actual on_change, because we do not want @@ -55,6 +52,10 @@ def __init__( self.placeholder = placeholder self.on_change = on_change + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.MultilineTextInput(interface=self) + @property def placeholder(self) -> str: """The placeholder text for the widget. diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 49d0e1c2f8..f5f4d051bc 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -114,9 +114,6 @@ def __init__( :param min_value: **DEPRECATED**; alias of ``min``. :param max_value: **DEPRECATED**; alias of ``max``. """ - self.factory = get_platform_factory() - self._impl = self.factory.NumberInput(interface=self) - super().__init__(id=id, style=style) ###################################################################### @@ -160,6 +157,10 @@ def __init__( self.on_change = on_change + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.NumberInput(interface=self) + @property def readonly(self) -> bool: """Can the value of the widget be modified by the user? diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 1441ab6786..443315e381 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -392,9 +392,6 @@ def __init__( ` to display in the OptionContainer. :param on_select: Initial :any:`on_select` handler. """ - self.factory = get_platform_factory() - self._impl = self.factory.OptionContainer(interface=self) - super().__init__(id=id, style=style) self._content = OptionList(self) self.on_select = None @@ -424,6 +421,10 @@ def __init__( self.on_select = on_select + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.OptionContainer(interface=self) + @property def enabled(self) -> bool: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/passwordinput.py b/core/src/toga/widgets/passwordinput.py index 48548b4935..249aafa466 100644 --- a/core/src/toga/widgets/passwordinput.py +++ b/core/src/toga/widgets/passwordinput.py @@ -1,5 +1,7 @@ from __future__ import annotations +from toga.platform import get_platform_factory + from .textinput import TextInput @@ -7,4 +9,5 @@ class PasswordInput(TextInput): """Create a new password input widget.""" def _create(self) -> None: + self.factory = get_platform_factory() self._impl = self.factory.PasswordInput(interface=self) diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index 8c2c7462c0..50c98077f6 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -32,9 +32,6 @@ def __init__( :param running: Describes whether the indicator is running at the time it is created. Default is False. """ - self.factory = get_platform_factory() - self._impl = self.factory.ProgressBar(interface=self) - super().__init__(id=id, style=style) self.max = max @@ -43,6 +40,10 @@ def __init__( if running: self.start() + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.ProgressBar(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 5f03dc4825..0155051496 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -41,9 +41,6 @@ def __init__( :param on_scroll: Initial :any:`on_scroll` handler. :param content: The content to display in the scroll window. """ - self.factory = get_platform_factory() - self._impl = self.factory.ScrollContainer(interface=self) - super().__init__(id=id, style=style) self._content: Widget | None = None @@ -55,6 +52,10 @@ def __init__( self.content = content self.on_scroll = on_scroll + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.ScrollContainer(interface=self) + @Widget.app.setter def app(self, app) -> None: # Invoke the superclass property setter diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 2ac193c9af..92646b2efd 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -49,9 +49,6 @@ def __init__( :param on_change: Initial :any:`on_change` handler. :param enabled: Whether the user can interact with the widget. """ - self.factory = get_platform_factory() - self._impl = self.factory.Selection(interface=self) - super().__init__(id=id, style=style) ###################################################################### @@ -82,6 +79,10 @@ def __init__( self.on_change = on_change self.enabled = enabled + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Selection(interface=self) + @property def items(self) -> SourceT | ListSource: """The items to display in the selection. diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index ce7d5a0c51..ba06ce4fca 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -40,6 +40,8 @@ def __call__(self, widget: Slider, /, **kwargs: Any) -> object: class Slider(Widget): + _MIN_WIDTH = 100 + def __init__( self, id: str | None = None, @@ -72,9 +74,6 @@ def __init__( :param range: **DEPRECATED**; use ``min`` and ``max`` instead. Initial :any:`range` of the slider. Defaults to ``(0, 1)``. """ - self.factory = get_platform_factory() - self._impl = self.factory.Slider(interface=self) - super().__init__(id=id, style=style) ###################################################################### @@ -117,7 +116,9 @@ def __init__( self.enabled = enabled - _MIN_WIDTH = 100 + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Slider(interface=self) # Backends are inconsistent about when they produce events for programmatic changes, # so we deal with those in the interface layer. diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index e3f591ed56..df215b7e8c 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -43,9 +43,6 @@ def __init__( :param content: Initial :any:`SplitContainer content ` of the container. Defaults to both panels being empty. """ - self.factory = get_platform_factory() - self._impl = self.factory.SplitContainer(interface=self) - super().__init__(id=id, style=style) self._content: list[SplitContainerContentT] = [None, None] @@ -53,6 +50,10 @@ def __init__( self.content = content self.direction = direction + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.SplitContainer(interface=self) + @property def enabled(self) -> bool: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index f001e67a0b..f7127a333a 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -40,9 +40,6 @@ def __init__( :param enabled: Is the switch enabled (i.e., can it be pressed?). Optional; by default, switches are created in an enabled state. """ - self.factory = get_platform_factory() - self._impl = self.factory.Switch(interface=self) - super().__init__(id=id, style=style) self.text = text @@ -56,6 +53,10 @@ def __init__( self.enabled = enabled + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Switch(interface=self) + @property def text(self) -> str: """The text label for the Switch. diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 4dd572c72f..7a62e28879 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -118,9 +118,6 @@ def __init__( self.on_activate = None self._data = None - self.factory = get_platform_factory() - self._impl = self.factory.Table(interface=self) - super().__init__(id=id, style=style) self.data = data @@ -128,6 +125,10 @@ def __init__( self.on_select = on_select self.on_activate = on_activate + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Table(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index f2f15fd334..43f7f12482 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -80,12 +80,6 @@ def __init__( input focus. :param validators: A list of validators to run on the value of the input. """ - self.factory = get_platform_factory() - self._impl = self.factory.TextInput(interface=self) - - self.factory = get_platform_factory() - self._create() - super().__init__(id=id, style=style) self.placeholder = placeholder @@ -108,6 +102,7 @@ def __init__( self.on_gain_focus = on_gain_focus def _create(self) -> None: + self.factory = get_platform_factory() self._impl = self.factory.TextInput(interface=self) @property diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 820382894f..b5cd51a011 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -41,9 +41,6 @@ def __init__( :param max: The latest time (inclusive) that can be selected. :param on_change: A handler that will be invoked when the value changes. """ - self.factory = get_platform_factory() - self._impl = self.factory.TimeInput(interface=self) - super().__init__(id=id, style=style) self.on_change = None @@ -53,6 +50,10 @@ def __init__( self.value = value self.on_change = on_change + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.TimeInput(interface=self) + @property def value(self) -> datetime.time: """The currently selected time. A value of ``None`` will be converted into the diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index c25d639483..21cc1e76a8 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -116,9 +116,6 @@ def __init__( self.on_activate = None self._data = None - self.factory = get_platform_factory() - self._impl = self.factory.Tree(interface=self) - super().__init__(id=id, style=style) self.data = data @@ -126,6 +123,10 @@ def __init__( self.on_select = on_select self.on_activate = on_activate + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.Tree(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index aab90e9ccf..a8a9715e1f 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -43,9 +43,6 @@ def __init__( :param on_webview_load: A handler that will be invoked when the web view finishes loading. """ - self.factory = get_platform_factory() - self._impl = self.factory.WebView(interface=self) - super().__init__(id=id, style=style) self.user_agent = user_agent @@ -54,6 +51,10 @@ def __init__( self.on_webview_load = on_webview_load self.url = url + def _create(self) -> None: + self.factory = get_platform_factory() + self._impl = self.factory.WebView(interface=self) + def _set_url(self, url: str | None, future: asyncio.Future | None) -> None: # Utility method for validating and setting the URL with a future. if (url is not None) and not url.startswith(("https://", "http://")): diff --git a/core/tests/utils.py b/core/tests/utils.py index e9bd314fa3..0fa03cba9c 100644 --- a/core/tests/utils.py +++ b/core/tests/utils.py @@ -6,13 +6,14 @@ # allow children class ExampleWidget(toga.Widget): def __init__(self, *args, **kwargs): - self.factory = get_platform_factory() - self._impl = self.factory.Widget(self) - super().__init__(*args, **kwargs) self._children = [] + def _create(self): + self.factory = get_platform_factory() + self._impl = self.factory.Widget(interface=self) + def __repr__(self): return f"Widget(id={self.id!r})" @@ -20,11 +21,9 @@ def __repr__(self): # Create the simplest possible widget with a concrete implementation that cannot # have children. class ExampleLeafWidget(toga.Widget): - def __init__(self, *args, **kwargs): + def _create(self): self.factory = get_platform_factory() - self._impl = self.factory.Widget(self) - - super().__init__(*args, **kwargs) + self._impl = self.factory.Widget(interface=self) def __repr__(self): return f"Widget(id={self.id!r})" From 030b719bd9bd5f3760c2b6f1753c1327bee0cc5c Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 14 Nov 2024 20:52:15 -0500 Subject: [PATCH 06/15] Remove stray _IMPL_NAMEs and put _create after priming attributes --- core/src/toga/widgets/base.py | 8 +++----- core/src/toga/widgets/dateinput.py | 2 -- core/src/toga/widgets/mapview.py | 2 -- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index a0b046246b..1faab5c473 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -16,8 +16,6 @@ class Widget(Node): - _IMPL_NAME = "Widget" - _MIN_WIDTH = 100 _MIN_HEIGHT = 100 @@ -39,13 +37,13 @@ def __init__( applicator=None, ) - # Create and assign _impl - self._create() - self._id = str(id if id else identifier(self)) self._window: Window | None = None self._app: App | None = None + # Create and assign _impl + self._create() + self.applicator = TogaApplicator() ############################################## diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index bc144af678..41b75e3adb 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -28,8 +28,6 @@ def __call__(self, widget: DateInput, /, **kwargs: Any) -> object: class DateInput(Widget): - _IMPL_NAME = "DateInput" - _MIN_WIDTH = 200 def __init__( diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 905381c6d3..c89c0d5b05 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -11,8 +11,6 @@ class MapPin: - _IMPL_NAME = "MapPin" - def __init__( self, location: toga.LatLng | tuple[float, float], From 336638733819d1524ba8eaa93b73de7c51efe6a5 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 15 Nov 2024 18:45:08 -0500 Subject: [PATCH 07/15] Issue warning to aid in migration to _create() --- changes/2942.removal.rst | 1 + core/src/toga/widgets/base.py | 11 +++++++---- core/tests/widgets/test_base.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 changes/2942.removal.rst diff --git a/changes/2942.removal.rst b/changes/2942.removal.rst new file mode 100644 index 0000000000..5fe3753a53 --- /dev/null +++ b/changes/2942.removal.rst @@ -0,0 +1 @@ +Widgets now create and assign their implementations via a ``_create()`` method. A user-created custom widget that inherits from an existing Toga widget and uses its same implementation will require no changes; any user-created widgets that need to specify their own implementation should do so in ``_create()``. Existing user code inheriting from Widget that creates its implementation before calling ``super().__init__()`` will continue to function, but give a RuntimeWarning; unfortunately, this change breaks any existing code that doesn't create its implementation until afterward. Such usage will now raise an exception. diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 1faab5c473..81768c8849 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -2,6 +2,7 @@ from builtins import id as identifier from typing import TYPE_CHECKING, TypeVar +from warnings import warn from travertino.declaration import BaseStyle from travertino.node import Node @@ -68,10 +69,12 @@ def __init__( ############################# def _create(self) -> None: - raise NotImplementedError( - "Widget must define a _create method to create and assign its " - "implementation." - ) # pragma: no cover + warn( + "Widgets should create their implementation and assign it to self._impl in " + "._create(). This will be an exception in a future version.", + RuntimeWarning, + stacklevel=2, + ) def __repr__(self) -> str: return f"<{self.__class__.__name__}:0x{identifier(self):x}>" diff --git a/core/tests/widgets/test_base.py b/core/tests/widgets/test_base.py index 494995a832..49c5b5bc2e 100644 --- a/core/tests/widgets/test_base.py +++ b/core/tests/widgets/test_base.py @@ -3,6 +3,7 @@ import pytest import toga +from toga.platform import get_platform_factory from toga.style import Pack from toga_dummy.utils import ( EventLog, @@ -16,6 +17,22 @@ from ..utils import ExampleLeafWidget, ExampleWidget +# Represent a hypothetical user-created widget class that does create and assign a valid +# implementation, but doesn't do so in _create(). This is to assist with migration for +# existing user code written before #2942 reorganized widget initialization. +# +# Right now this only issues a warning. Unfortunately it only works if the _impl is +# set *before* super().__init__; any existing code that does so afterward will raise an +# exception. +class WidgetSubclassWithoutCreate(toga.Widget): + def __init__(self, *args, **kwargs): + + self.factory = get_platform_factory() + self._impl = self.factory.Widget(interface=self) + + super().__init__(*args, **kwargs) + + @pytest.fixture def widget(app): # App fixture is needed to ensure a fresh widget registry is created @@ -1228,11 +1245,19 @@ def test_tab_index(widget): def test_one_reapply_during_init(): - # The style's reapply() method should be called exactly once during widget - # initialization. + """Style's reapply() should be called exactly once during widget initialization.""" class MockedPack(Pack): reapply = Mock() ExampleWidget(style=MockedPack()) MockedPack.reapply.assert_called_once() + + +def test_widget_with_no_create(): + """Creating a widget with no _create() method issues a warning.""" + with pytest.warns( + RuntimeWarning, + match=r"Widgets should create their implementation", + ): + WidgetSubclassWithoutCreate() From a1230fa38a714f0a8f5815413ff67c05419d8c95 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 15 Nov 2024 18:47:33 -0500 Subject: [PATCH 08/15] Commented out Textual? --- testbed/tests/testbed.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/testbed/tests/testbed.py b/testbed/tests/testbed.py index 94f654e34d..d99ed69d8e 100644 --- a/testbed/tests/testbed.py +++ b/testbed/tests/testbed.py @@ -40,10 +40,14 @@ def run_tests(app, cov, args, report_coverage, run_slow, running_in_ci): # Textual backend does not yet support testing. # However, this will verify a Textual app can at least start. - if app.factory.__name__.startswith("toga_textual"): - time.sleep(1) # wait for the Textual app to start - app.returncode = 0 if app._impl.native.is_running else 1 - return + # + # This is temporarily commented out, as of #2942, until Textual is implemented + # enough to survive the Pack.reapply() when the label is created. + # + # if app.factory.__name__.startswith("toga_textual"): + # time.sleep(1) # wait for the Textual app to start + # app.returncode = 0 if app._impl.native.is_running else 1 + # return # Control the run speed of the test app. app.run_slow = run_slow From 45b5f7186b49501833271733fc96091d00ac15c2 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 15 Nov 2024 19:04:04 -0500 Subject: [PATCH 09/15] Reverted Textual skip --- testbed/tests/testbed.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/testbed/tests/testbed.py b/testbed/tests/testbed.py index d99ed69d8e..94f654e34d 100644 --- a/testbed/tests/testbed.py +++ b/testbed/tests/testbed.py @@ -40,14 +40,10 @@ def run_tests(app, cov, args, report_coverage, run_slow, running_in_ci): # Textual backend does not yet support testing. # However, this will verify a Textual app can at least start. - # - # This is temporarily commented out, as of #2942, until Textual is implemented - # enough to survive the Pack.reapply() when the label is created. - # - # if app.factory.__name__.startswith("toga_textual"): - # time.sleep(1) # wait for the Textual app to start - # app.returncode = 0 if app._impl.native.is_running else 1 - # return + if app.factory.__name__.startswith("toga_textual"): + time.sleep(1) # wait for the Textual app to start + app.returncode = 0 if app._impl.native.is_running else 1 + return # Control the run speed of the test app. app.run_slow = run_slow From eb1852e1c7a4f3258cee7065b4c317ea9d78c8d8 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 15 Nov 2024 19:29:02 -0500 Subject: [PATCH 10/15] Added NotImplemented safeguard for Font creation in Pack.apply --- core/src/toga/style/pack.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 8743293968..2a80b2b0ff 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -118,15 +118,22 @@ def apply(self, prop: str, value: object) -> None: "font_variant", "font_weight", ): - self._applicator.set_font( - Font( + # For other properties, a backend that doesn't support it will simply do + # a no-op when instructed to set it. But for font, we need to know + # up-front, because just creating a Font object will fail. + try: + font = Font( self.font_family, self.font_size, style=self.font_style, variant=self.font_variant, weight=self.font_weight, ) - ) + except NotImplementedError: # pragma: no cover + font = None + + if font: # pragma: no branch + self._applicator.set_font(font) else: # Any other style change will cause a change in layout geometry, # so perform a refresh. From c4db37f5a5c82bf1a4ba7ea51351803e803239fd Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 15 Nov 2024 19:41:13 -0500 Subject: [PATCH 11/15] Added _create() docstring --- cocoa/src/toga_cocoa/widgets/button.py | 1 - core/src/toga/widgets/base.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/widgets/button.py b/cocoa/src/toga_cocoa/widgets/button.py index bd42e6ebc7..eadb8e2b92 100644 --- a/cocoa/src/toga_cocoa/widgets/button.py +++ b/cocoa/src/toga_cocoa/widgets/button.py @@ -32,7 +32,6 @@ def create(self): self._icon = None self.native.buttonType = NSMomentaryPushInButton - # self._set_button_style() self.native.target = self.native self.native.action = SEL("onPress:") diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 81768c8849..fa5407e06e 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -69,6 +69,11 @@ def __init__( ############################# def _create(self) -> None: + """Create and store a platform-specific implementation of this widget. + + A subclass of Widget should redefine this method to create an implementation + and assign it to self._impl. + """ warn( "Widgets should create their implementation and assign it to self._impl in " "._create(). This will be an exception in a future version.", From b2735bacde5471dc719480f151817c190f38326d Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 28 Nov 2024 23:20:10 -0500 Subject: [PATCH 12/15] Reorganized _create, removed applicator=None, "implemented" Font in Textual --- core/src/toga/style/pack.py | 13 ++----- core/src/toga/widgets/activityindicator.py | 7 +--- core/src/toga/widgets/base.py | 41 +++++++++++++++------ core/src/toga/widgets/box.py | 7 +--- core/src/toga/widgets/button.py | 6 +-- core/src/toga/widgets/canvas.py | 6 +-- core/src/toga/widgets/dateinput.py | 6 +-- core/src/toga/widgets/detailedlist.py | 6 +-- core/src/toga/widgets/divider.py | 6 +-- core/src/toga/widgets/imageview.py | 6 +-- core/src/toga/widgets/label.py | 7 +--- core/src/toga/widgets/mapview.py | 6 +-- core/src/toga/widgets/multilinetextinput.py | 6 +-- core/src/toga/widgets/numberinput.py | 6 +-- core/src/toga/widgets/optioncontainer.py | 5 +-- core/src/toga/widgets/passwordinput.py | 7 +--- core/src/toga/widgets/progressbar.py | 7 +--- core/src/toga/widgets/scrollcontainer.py | 6 +-- core/src/toga/widgets/selection.py | 6 +-- core/src/toga/widgets/slider.py | 6 +-- core/src/toga/widgets/splitcontainer.py | 6 +-- core/src/toga/widgets/switch.py | 6 +-- core/src/toga/widgets/table.py | 6 +-- core/src/toga/widgets/textinput.py | 6 +-- core/src/toga/widgets/timeinput.py | 6 +-- core/src/toga/widgets/tree.py | 6 +-- core/src/toga/widgets/webview.py | 6 +-- core/tests/utils.py | 7 +--- core/tests/widgets/test_base.py | 2 +- textual/src/toga_textual/factory.py | 5 +-- textual/src/toga_textual/fonts.py | 2 + 31 files changed, 89 insertions(+), 135 deletions(-) create mode 100644 textual/src/toga_textual/fonts.py diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 9a7ae20892..57db86b12a 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -127,22 +127,15 @@ def apply(self, prop: str, value: object) -> None: "font_variant", "font_weight", ): - # For other properties, a backend that doesn't support it will simply do - # a no-op when instructed to set it. But for font, we need to know - # up-front, because just creating a Font object will fail. - try: - font = Font( + self._applicator.set_font( + Font( self.font_family, self.font_size, style=self.font_style, variant=self.font_variant, weight=self.font_weight, ) - except NotImplementedError: # pragma: no cover - font = None - - if font: # pragma: no branch - self._applicator.set_font(font) + ) else: # Any other style change will cause a change in layout geometry, # so perform a refresh. diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index 1fee04f06b..72a2742663 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -2,8 +2,6 @@ from typing import Literal -from toga.platform import get_platform_factory - from .base import StyleT, Widget @@ -27,9 +25,8 @@ def __init__( if running: self.start() - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.ActivityIndicator(interface=self) + def _create(self) -> object: + return self.factory.ActivityIndicator(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 65e12af782..ba4f20ed11 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -7,6 +7,7 @@ from travertino.declaration import BaseStyle from travertino.node import Node +from toga.platform import get_platform_factory from toga.style import Pack, TogaApplicator if TYPE_CHECKING: @@ -33,17 +34,34 @@ def __init__( :param style: A style object. If no style is provided, a default style will be applied to the widget. """ - super().__init__( - style=style if style is not None else Pack(), - applicator=None, - ) + super().__init__(style=style if style is not None else Pack()) self._id = str(id if id else identifier(self)) self._window: Window | None = None self._app: App | None = None - # Create and assign _impl - self._create() + # Get factory and assign implementation + self.factory = get_platform_factory() + + ######################### + # Backwards compatibility + ######################### + + # Just in case we're working with a third-party widget created before + # the _create() mechanism was added, which has already defined its + # implementation. We still want to call _create(), to issue the warning and + # inform users about where they should be creating the implementation, but if + # there already is one, we don't want to do the assignment and thus replace it + # with None. + + impl = self._create() + + if not hasattr(self, "_impl"): + self._impl = impl + + ############################# + # End backwards compatibility + ############################# self.applicator = TogaApplicator() @@ -68,15 +86,14 @@ def __init__( # End backwards compatibility ############################# - def _create(self) -> None: - """Create and store a platform-specific implementation of this widget. + def _create(self) -> object: + """Create a platform-specific implementation of this widget. - A subclass of Widget should redefine this method to create an implementation - and assign it to self._impl. + A subclass of Widget should redefine this method to return its implementation. """ warn( - "Widgets should create their implementation and assign it to self._impl in " - "._create(). This will be an exception in a future version.", + "Widgets should create and return their implementation in ._create(). This " + "will be an exception in a future version.", RuntimeWarning, stacklevel=2, ) diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index 0944c2ff67..48ab114c78 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -2,8 +2,6 @@ from collections.abc import Iterable -from toga.platform import get_platform_factory - from .base import StyleT, Widget @@ -31,9 +29,8 @@ def __init__( if children is not None: self.add(*children) - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Box(interface=self) + def _create(self): + return self.factory.Box(interface=self) @property def enabled(self) -> bool: diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index c9d005b33b..789940dfab 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -4,7 +4,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -61,9 +60,8 @@ def __init__( self.on_press = on_press self.enabled = enabled - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Button(interface=self) + def _create(self) -> object: + return self.factory.Button(interface=self) @property def text(self) -> str: diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index d2c844e070..f4107480cd 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -25,7 +25,6 @@ Font, ) from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -1254,9 +1253,8 @@ def __init__( self.on_alt_release = on_alt_release self.on_alt_drag = on_alt_drag - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Canvas(interface=self) + def _create(self) -> object: + return self.factory.Canvas(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 41b75e3adb..1707f49de0 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -6,7 +6,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -59,9 +58,8 @@ def __init__( self.value = value self.on_change = on_change - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.DateInput(interface=self) + def _create(self) -> object: + return self.factory.DateInput(interface=self) @property def value(self) -> datetime.date: diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 5b9309f49e..92455b95a9 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -6,7 +6,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from toga.sources import ListSource, Row, Source from .base import StyleT, Widget @@ -121,9 +120,8 @@ def __init__( self.on_refresh = on_refresh self.on_select = on_select - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.DetailedList(interface=self) + def _create(self) -> object: + return self.factory.DetailedList(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index 3094f60673..1b099289c8 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -3,7 +3,6 @@ from typing import Literal from toga.constants import Direction -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -32,9 +31,8 @@ def __init__( self.direction = direction - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Divider(interface=self) + def _create(self) -> object: + return self.factory.Divider(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 2ad4c1b181..ed20738ab5 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -5,7 +5,6 @@ from travertino.size import at_least import toga -from toga.platform import get_platform_factory from toga.style.pack import NONE from toga.widgets.base import StyleT, Widget @@ -91,9 +90,8 @@ def __init__( self.image = image - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.ImageView(interface=self) + def _create(self) -> object: + return self.factory.ImageView(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 1d7ef6243d..52bbcd6d33 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -1,7 +1,5 @@ from __future__ import annotations -from toga.platform import get_platform_factory - from .base import StyleT, Widget @@ -23,9 +21,8 @@ def __init__( self.text = text - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Label(interface=self) + def _create(self) -> object: + return self.factory.Label(interface=self) def focus(self) -> None: """No-op; Label cannot accept input focus.""" diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index c89c0d5b05..50e1730684 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -5,7 +5,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -168,9 +167,8 @@ def __init__( self.on_select = on_select - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.MapView(interface=self) + def _create(self) -> object: + return self.factory.MapView(interface=self) @property def location(self) -> toga.LatLng: diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 9c0c4a2ccd..26521ba560 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -4,7 +4,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -52,9 +51,8 @@ def __init__( self.placeholder = placeholder self.on_change = on_change - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.MultilineTextInput(interface=self) + def _create(self) -> object: + return self.factory.MultilineTextInput(interface=self) @property def placeholder(self) -> str: diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index f5f4d051bc..9d102049b5 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -8,7 +8,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -157,9 +156,8 @@ def __init__( self.on_change = on_change - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.NumberInput(interface=self) + def _create(self) -> object: + return self.factory.NumberInput(interface=self) @property def readonly(self) -> bool: diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 253761fcf8..b234f91236 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -422,9 +422,8 @@ def __init__( self.on_select = on_select - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.OptionContainer(interface=self) + def _create(self) -> object: + return self.factory.OptionContainer(interface=self) @property def enabled(self) -> bool: diff --git a/core/src/toga/widgets/passwordinput.py b/core/src/toga/widgets/passwordinput.py index 249aafa466..6ae05daa65 100644 --- a/core/src/toga/widgets/passwordinput.py +++ b/core/src/toga/widgets/passwordinput.py @@ -1,13 +1,10 @@ from __future__ import annotations -from toga.platform import get_platform_factory - from .textinput import TextInput class PasswordInput(TextInput): """Create a new password input widget.""" - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.PasswordInput(interface=self) + def _create(self) -> object: + return self.factory.PasswordInput(interface=self) diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index 50c98077f6..185031d71c 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -2,8 +2,6 @@ from typing import Literal, SupportsFloat -from toga.platform import get_platform_factory - from .base import StyleT, Widget @@ -40,9 +38,8 @@ def __init__( if running: self.start() - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.ProgressBar(interface=self) + def _create(self) -> object: + return self.factory.ProgressBar(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 3e69759a42..2be41ae48a 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Any, Literal, Protocol, SupportsInt from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from toga.types import Position from .base import StyleT, Widget @@ -52,9 +51,8 @@ def __init__( self.content = content self.on_scroll = on_scroll - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.ScrollContainer(interface=self) + def _create(self) -> object: + return self.factory.ScrollContainer(interface=self) @Widget.app.setter def app(self, app) -> None: diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 95b4a13ab7..4c3ae7bebe 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -6,7 +6,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from toga.sources import ListSource, Source from .base import StyleT, Widget @@ -79,9 +78,8 @@ def __init__( self.on_change = on_change self.enabled = enabled - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Selection(interface=self) + def _create(self) -> object: + return self.factory.Selection(interface=self) @property def items(self) -> SourceT | ListSource: diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 7cde020b83..6c22bfdda9 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -7,7 +7,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -117,9 +116,8 @@ def __init__( self.enabled = enabled - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Slider(interface=self) + def _create(self) -> object: + return self.factory.Slider(interface=self) # Backends are inconsistent about when they produce events for programmatic changes, # so we deal with those in the interface layer. diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index df215b7e8c..61bc065855 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -6,7 +6,6 @@ from toga.app import App from toga.constants import Direction -from toga.platform import get_platform_factory from toga.window import Window from .base import StyleT, Widget @@ -50,9 +49,8 @@ def __init__( self.content = content self.direction = direction - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.SplitContainer(interface=self) + def _create(self) -> object: + return self.factory.SplitContainer(interface=self) @property def enabled(self) -> bool: diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 42e89f8f93..da2c93302d 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -4,7 +4,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -53,9 +52,8 @@ def __init__( self.enabled = enabled - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Switch(interface=self) + def _create(self) -> object: + return self.factory.Switch(interface=self) @property def text(self) -> str: diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 7a62e28879..24aa05fa25 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -6,7 +6,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from toga.sources import ListSource, Row, Source from toga.sources.accessors import build_accessors, to_accessor @@ -125,9 +124,8 @@ def __init__( self.on_select = on_select self.on_activate = on_activate - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Table(interface=self) + def _create(self) -> object: + return self.factory.Table(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 43f7f12482..1464b4de88 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -5,7 +5,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -101,9 +100,8 @@ def __init__( self.on_lose_focus = on_lose_focus self.on_gain_focus = on_gain_focus - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.TextInput(interface=self) + def _create(self) -> object: + return self.factory.TextInput(interface=self) @property def readonly(self) -> bool: diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index b5cd51a011..b94cd945eb 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -6,7 +6,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -50,9 +49,8 @@ def __init__( self.value = value self.on_change = on_change - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.TimeInput(interface=self) + def _create(self) -> object: + return self.factory.TimeInput(interface=self) @property def value(self) -> datetime.time: diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 21cc1e76a8..a4d5ceb862 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -6,7 +6,6 @@ import toga from toga.handlers import wrapped_handler -from toga.platform import get_platform_factory from toga.sources import Node, Source, TreeSource from toga.sources.accessors import build_accessors, to_accessor from toga.style import Pack @@ -123,9 +122,8 @@ def __init__( self.on_select = on_select self.on_activate = on_activate - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.Tree(interface=self) + def _create(self): + return self.factory.Tree(interface=self) @property def enabled(self) -> Literal[True]: diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index a8a9715e1f..6d3e9f2ae9 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -4,7 +4,6 @@ from typing import Any, Protocol from toga.handlers import AsyncResult, OnResultT, wrapped_handler -from toga.platform import get_platform_factory from .base import StyleT, Widget @@ -51,9 +50,8 @@ def __init__( self.on_webview_load = on_webview_load self.url = url - def _create(self) -> None: - self.factory = get_platform_factory() - self._impl = self.factory.WebView(interface=self) + def _create(self) -> object: + return self.factory.WebView(interface=self) def _set_url(self, url: str | None, future: asyncio.Future | None) -> None: # Utility method for validating and setting the URL with a future. diff --git a/core/tests/utils.py b/core/tests/utils.py index 0fa03cba9c..1021c67cb7 100644 --- a/core/tests/utils.py +++ b/core/tests/utils.py @@ -1,5 +1,4 @@ import toga -from toga.platform import get_platform_factory # Create the simplest possible widget with a concrete implementation that will @@ -11,8 +10,7 @@ def __init__(self, *args, **kwargs): self._children = [] def _create(self): - self.factory = get_platform_factory() - self._impl = self.factory.Widget(interface=self) + return self.factory.Widget(interface=self) def __repr__(self): return f"Widget(id={self.id!r})" @@ -22,8 +20,7 @@ def __repr__(self): # have children. class ExampleLeafWidget(toga.Widget): def _create(self): - self.factory = get_platform_factory() - self._impl = self.factory.Widget(interface=self) + return self.factory.Widget(interface=self) def __repr__(self): return f"Widget(id={self.id!r})" diff --git a/core/tests/widgets/test_base.py b/core/tests/widgets/test_base.py index 49c5b5bc2e..9052bdf095 100644 --- a/core/tests/widgets/test_base.py +++ b/core/tests/widgets/test_base.py @@ -1258,6 +1258,6 @@ def test_widget_with_no_create(): """Creating a widget with no _create() method issues a warning.""" with pytest.warns( RuntimeWarning, - match=r"Widgets should create their implementation", + match=r"Widgets should create and return their implementation", ): WidgetSubclassWithoutCreate() diff --git a/textual/src/toga_textual/factory.py b/textual/src/toga_textual/factory.py index b737aed521..d41f6da7fa 100644 --- a/textual/src/toga_textual/factory.py +++ b/textual/src/toga_textual/factory.py @@ -3,8 +3,7 @@ from . import dialogs from .app import App from .command import Command - -# from .fonts import Font +from .fonts import Font from .icons import Icon # from .images import Image @@ -50,7 +49,7 @@ def not_implemented(feature): "not_implemented", "App", "Command", - # "Font", + "Font", "Icon", # "Image", "Paths", diff --git a/textual/src/toga_textual/fonts.py b/textual/src/toga_textual/fonts.py new file mode 100644 index 0000000000..fa9d3f0e6c --- /dev/null +++ b/textual/src/toga_textual/fonts.py @@ -0,0 +1,2 @@ +class Font: + def __init__(*args, **kwargs): ... From 3ec3a91a2ab9b5f49f7e31889b15c159aea11e9b Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Thu, 28 Nov 2024 23:46:37 -0500 Subject: [PATCH 13/15] Better type hint for _create --- core/src/toga/widgets/activityindicator.py | 4 ++-- core/src/toga/widgets/base.py | 4 ++-- core/src/toga/widgets/button.py | 2 +- core/src/toga/widgets/canvas.py | 2 +- core/src/toga/widgets/dateinput.py | 2 +- core/src/toga/widgets/detailedlist.py | 2 +- core/src/toga/widgets/divider.py | 4 ++-- core/src/toga/widgets/imageview.py | 4 ++-- core/src/toga/widgets/label.py | 4 +++- core/src/toga/widgets/mapview.py | 2 +- core/src/toga/widgets/multilinetextinput.py | 2 +- core/src/toga/widgets/numberinput.py | 2 +- core/src/toga/widgets/optioncontainer.py | 2 +- core/src/toga/widgets/passwordinput.py | 4 +++- core/src/toga/widgets/progressbar.py | 4 ++-- core/src/toga/widgets/scrollcontainer.py | 2 +- core/src/toga/widgets/selection.py | 2 +- core/src/toga/widgets/slider.py | 2 +- core/src/toga/widgets/splitcontainer.py | 4 ++-- core/src/toga/widgets/switch.py | 2 +- core/src/toga/widgets/table.py | 2 +- core/src/toga/widgets/textinput.py | 2 +- core/src/toga/widgets/timeinput.py | 2 +- core/src/toga/widgets/webview.py | 2 +- 24 files changed, 34 insertions(+), 30 deletions(-) diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index 72a2742663..4ac5c2ab18 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal from .base import StyleT, Widget @@ -25,7 +25,7 @@ def __init__( if running: self.start() - def _create(self) -> object: + def _create(self) -> Any: return self.factory.ActivityIndicator(interface=self) @property diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index ba4f20ed11..d19818991c 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -1,7 +1,7 @@ from __future__ import annotations from builtins import id as identifier -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from warnings import warn from travertino.declaration import BaseStyle @@ -86,7 +86,7 @@ def __init__( # End backwards compatibility ############################# - def _create(self) -> object: + def _create(self) -> Any: """Create a platform-specific implementation of this widget. A subclass of Widget should redefine this method to return its implementation. diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index 789940dfab..0c07a76d15 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -60,7 +60,7 @@ def __init__( self.on_press = on_press self.enabled = enabled - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Button(interface=self) @property diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index f4107480cd..355b400144 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1253,7 +1253,7 @@ def __init__( self.on_alt_release = on_alt_release self.on_alt_drag = on_alt_drag - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Canvas(interface=self) @property diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 1707f49de0..b277023bf8 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -58,7 +58,7 @@ def __init__( self.value = value self.on_change = on_change - def _create(self) -> object: + def _create(self) -> Any: return self.factory.DateInput(interface=self) @property diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 92455b95a9..434c08eab5 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -120,7 +120,7 @@ def __init__( self.on_refresh = on_refresh self.on_select = on_select - def _create(self) -> object: + def _create(self) -> Any: return self.factory.DetailedList(interface=self) @property diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index 1b099289c8..8a7db69fe1 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal from toga.constants import Direction @@ -31,7 +31,7 @@ def __init__( self.direction = direction - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Divider(interface=self) @property diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index ed20738ab5..7011e059ab 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal from travertino.size import at_least @@ -90,7 +90,7 @@ def __init__( self.image = image - def _create(self) -> object: + def _create(self) -> Any: return self.factory.ImageView(interface=self) @property diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 52bbcd6d33..b448ae5905 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from .base import StyleT, Widget @@ -21,7 +23,7 @@ def __init__( self.text = text - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Label(interface=self) def focus(self) -> None: diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 50e1730684..425a5a65ab 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -167,7 +167,7 @@ def __init__( self.on_select = on_select - def _create(self) -> object: + def _create(self) -> Any: return self.factory.MapView(interface=self) @property diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 26521ba560..c966ad036b 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -51,7 +51,7 @@ def __init__( self.placeholder = placeholder self.on_change = on_change - def _create(self) -> object: + def _create(self) -> Any: return self.factory.MultilineTextInput(interface=self) @property diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 9d102049b5..d05edf937f 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -156,7 +156,7 @@ def __init__( self.on_change = on_change - def _create(self) -> object: + def _create(self) -> Any: return self.factory.NumberInput(interface=self) @property diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index b234f91236..8a90f64d36 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -422,7 +422,7 @@ def __init__( self.on_select = on_select - def _create(self) -> object: + def _create(self) -> Any: return self.factory.OptionContainer(interface=self) @property diff --git a/core/src/toga/widgets/passwordinput.py b/core/src/toga/widgets/passwordinput.py index 6ae05daa65..b72bcba0f0 100644 --- a/core/src/toga/widgets/passwordinput.py +++ b/core/src/toga/widgets/passwordinput.py @@ -1,10 +1,12 @@ from __future__ import annotations +from typing import Any + from .textinput import TextInput class PasswordInput(TextInput): """Create a new password input widget.""" - def _create(self) -> object: + def _create(self) -> Any: return self.factory.PasswordInput(interface=self) diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index 185031d71c..0d8e1cebcb 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal, SupportsFloat +from typing import Any, Literal, SupportsFloat from .base import StyleT, Widget @@ -38,7 +38,7 @@ def __init__( if running: self.start() - def _create(self) -> object: + def _create(self) -> Any: return self.factory.ProgressBar(interface=self) @property diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 2be41ae48a..a69e9d2673 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -51,7 +51,7 @@ def __init__( self.content = content self.on_scroll = on_scroll - def _create(self) -> object: + def _create(self) -> Any: return self.factory.ScrollContainer(interface=self) @Widget.app.setter diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 4c3ae7bebe..667877d35b 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -78,7 +78,7 @@ def __init__( self.on_change = on_change self.enabled = enabled - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Selection(interface=self) @property diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 6c22bfdda9..2ebd29fa00 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -116,7 +116,7 @@ def __init__( self.enabled = enabled - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Slider(interface=self) # Backends are inconsistent about when they produce events for programmatic changes, diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 61bc065855..29493e856e 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -2,7 +2,7 @@ import sys from collections.abc import Sequence -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from toga.app import App from toga.constants import Direction @@ -49,7 +49,7 @@ def __init__( self.content = content self.direction = direction - def _create(self) -> object: + def _create(self) -> Any: return self.factory.SplitContainer(interface=self) @property diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index da2c93302d..927e45a8d1 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -52,7 +52,7 @@ def __init__( self.enabled = enabled - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Switch(interface=self) @property diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 24aa05fa25..f450c77d4d 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -124,7 +124,7 @@ def __init__( self.on_select = on_select self.on_activate = on_activate - def _create(self) -> object: + def _create(self) -> Any: return self.factory.Table(interface=self) @property diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 1464b4de88..20b62f075d 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -100,7 +100,7 @@ def __init__( self.on_lose_focus = on_lose_focus self.on_gain_focus = on_gain_focus - def _create(self) -> object: + def _create(self) -> Any: return self.factory.TextInput(interface=self) @property diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index b94cd945eb..3ae7e13e3f 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -49,7 +49,7 @@ def __init__( self.value = value self.on_change = on_change - def _create(self) -> object: + def _create(self) -> Any: return self.factory.TimeInput(interface=self) @property diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 6d3e9f2ae9..64d5a641bd 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -50,7 +50,7 @@ def __init__( self.on_webview_load = on_webview_load self.url = url - def _create(self) -> object: + def _create(self) -> Any: return self.factory.WebView(interface=self) def _set_url(self, url: str | None, future: asyncio.Future | None) -> None: From fb7d3f9374fb99cb7de435ae699bd96261d04b15 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Fri, 29 Nov 2024 01:43:59 -0500 Subject: [PATCH 14/15] Updated changenote to reflect change to _create --- changes/2942.removal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/2942.removal.rst b/changes/2942.removal.rst index 5fe3753a53..365ef380c1 100644 --- a/changes/2942.removal.rst +++ b/changes/2942.removal.rst @@ -1 +1 @@ -Widgets now create and assign their implementations via a ``_create()`` method. A user-created custom widget that inherits from an existing Toga widget and uses its same implementation will require no changes; any user-created widgets that need to specify their own implementation should do so in ``_create()``. Existing user code inheriting from Widget that creates its implementation before calling ``super().__init__()`` will continue to function, but give a RuntimeWarning; unfortunately, this change breaks any existing code that doesn't create its implementation until afterward. Such usage will now raise an exception. +Widgets now create and return their implementations via a ``_create()`` method. A user-created custom widget that inherits from an existing Toga widget and uses its same implementation will require no changes; any user-created widgets that need to specify their own implementation should do so in ``_create()`` and return it. Existing user code inheriting from Widget that assigns its implementation before calling ``super().__init__()`` will continue to function, but give a RuntimeWarning; unfortunately, this change breaks any existing code that doesn't create its implementation until afterward. Such usage will now raise an exception. From a3f9a546c7a4b6f17720d9373345204b50fe4d57 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 1 Dec 2024 21:29:00 -0500 Subject: [PATCH 15/15] Add version to backwards compat comment Co-authored-by: Russell Keith-Magee --- core/src/toga/widgets/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index d19818991c..0d179ac8ac 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -43,9 +43,9 @@ def __init__( # Get factory and assign implementation self.factory = get_platform_factory() - ######################### - # Backwards compatibility - ######################### + ########################################### + # Backwards compatibility for Toga <= 0.4.8 + ########################################### # Just in case we're working with a third-party widget created before # the _create() mechanism was added, which has already defined its