From 5fe9211be57eb6c55dfc1f292d87569da5ceeefc Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Wed, 29 Nov 2023 19:26:29 -0500 Subject: [PATCH 01/13] Typing ergonomics and guarantees --- .github/workflows/ci.yml | 42 +++ .gitignore | 1 + changes/2252.misc.rst | 1 + core/pyproject.toml | 1 + core/src/toga/__init__.py | 9 +- core/src/toga/app.py | 207 ++++++----- core/src/toga/command.py | 104 +++--- core/src/toga/constants/__init__.py | 11 +- core/src/toga/documents.py | 15 +- core/src/toga/fonts.py | 20 +- core/src/toga/handlers.py | 81 +++- core/src/toga/hardware/camera.py | 8 +- core/src/toga/icons.py | 33 +- core/src/toga/images.py | 33 +- core/src/toga/keys.py | 8 +- core/src/toga/paths.py | 6 +- core/src/toga/platform.py | 11 +- core/src/toga/plugins/image_formats.py | 2 +- core/src/toga/screens.py | 6 +- core/src/toga/sources/__init__.py | 11 + core/src/toga/sources/accessors.py | 5 +- core/src/toga/sources/base.py | 20 +- core/src/toga/sources/list_source.py | 48 ++- core/src/toga/sources/tree_source.py | 108 ++++-- core/src/toga/sources/value_source.py | 6 +- core/src/toga/style/__init__.py | 5 + core/src/toga/style/applicator.py | 24 +- core/src/toga/style/pack.py | 55 +-- core/src/toga/types.py | 11 +- core/src/toga/validators.py | 20 +- core/src/toga/widgets/activityindicator.py | 10 +- core/src/toga/widgets/base.py | 21 +- core/src/toga/widgets/box.py | 10 +- core/src/toga/widgets/button.py | 45 ++- core/src/toga/widgets/canvas.py | 345 ++++++++++-------- core/src/toga/widgets/dateinput.py | 89 +++-- core/src/toga/widgets/detailedlist.py | 201 +++++++--- core/src/toga/widgets/divider.py | 19 +- core/src/toga/widgets/imageview.py | 33 +- core/src/toga/widgets/label.py | 12 +- core/src/toga/widgets/mapview.py | 77 ++-- core/src/toga/widgets/multilinetextinput.py | 54 ++- core/src/toga/widgets/numberinput.py | 104 ++++-- core/src/toga/widgets/optioncontainer.py | 170 +++++---- core/src/toga/widgets/passwordinput.py | 2 +- core/src/toga/widgets/progressbar.py | 30 +- core/src/toga/widgets/scrollcontainer.py | 79 ++-- core/src/toga/widgets/selection.py | 89 +++-- core/src/toga/widgets/slider.py | 185 +++++++--- core/src/toga/widgets/splitcontainer.py | 32 +- core/src/toga/widgets/switch.py | 48 ++- core/src/toga/widgets/table.py | 132 ++++--- core/src/toga/widgets/textinput.py | 175 ++++++--- core/src/toga/widgets/timeinput.py | 87 +++-- core/src/toga/widgets/tree.py | 135 ++++--- core/src/toga/widgets/webview.py | 65 +++- core/src/toga/window.py | 255 +++++++++---- docs/reference/api/app.rst | 8 +- .../api/containers/optioncontainer.rst | 12 +- .../api/containers/scrollcontainer.rst | 4 + docs/reference/api/resources/command.rst | 4 +- docs/reference/api/widgets/button.rst | 4 +- docs/reference/api/widgets/dateinput.rst | 4 + docs/reference/api/widgets/detailedlist.rst | 13 + docs/reference/api/widgets/mapview.rst | 5 +- .../api/widgets/multilinetextinput.rst | 4 + docs/reference/api/widgets/numberinput.rst | 4 + docs/reference/api/widgets/slider.rst | 10 + docs/reference/api/widgets/switch.rst | 4 + docs/reference/api/widgets/table.rst | 7 + docs/reference/api/widgets/textinput.rst | 13 + docs/reference/api/widgets/timeinput.rst | 4 + docs/reference/api/widgets/tree.rst | 7 + docs/reference/api/widgets/webview.rst | 4 + docs/reference/api/window.rst | 15 +- docs/spelling_wordlist | 1 + pyproject.toml | 33 ++ tox.ini | 9 + 78 files changed, 2354 insertions(+), 1231 deletions(-) create mode 100644 changes/2252.misc.rst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 929fc91893..feff86b2ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,36 @@ jobs: build-subdirectory: ${{ matrix.subdir }} attest: ${{ inputs.attest-package }} + types: + name: Types + runs-on: ubuntu-latest + needs: [ pre-commit, towncrier, package ] + steps: + - name: Checkout Toga + uses: actions/checkout@v4.1.2 + + - name: Checkout beeware/.github + uses: actions/checkout@v4.1.2 + with: + repository: beeware/.github + path: .github + + - name: Set up Python ${{ env.min_python_version }} + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ env.min_python_version }} + + - name: Install tox + working-directory: .github/scripts + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools build wheel + # Utility script installs tox as defined in core/pyproject.toml + python -m install_requirement tox --extra dev --project-root ${{ github.workspace }}/core + + - name: Type Checks + run: tox -e types + core: name: Test core runs-on: ${{ matrix.platform }} @@ -105,6 +135,12 @@ jobs: with: fetch-depth: 0 + - name: Checkout beeware/.github + uses: actions/checkout@v4.1.2 + with: + repository: beeware/.github + path: .github + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5.1.0 with: @@ -150,6 +186,12 @@ jobs: with: fetch-depth: 0 + - name: Checkout beeware/.github + uses: actions/checkout@v4.1.2 + with: + repository: beeware/.github + path: .github + - name: Set up Python ${{ env.min_python_version }} uses: actions/setup-python@v5.1.0 with: diff --git a/.gitignore b/.gitignore index 95f5bd0ed5..c47413608b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ pyvenv.cfg .envrc bin/ lib/ +.dmypy.json # Exclude briefcase packages from the examples dir. examples/*/macOS/ diff --git a/changes/2252.misc.rst b/changes/2252.misc.rst new file mode 100644 index 0000000000..4a8852817f --- /dev/null +++ b/changes/2252.misc.rst @@ -0,0 +1 @@ +The typing for Toga's API surface is now compliant with mypy. diff --git a/core/pyproject.toml b/core/pyproject.toml index 3d09110f62..aa52cc3799 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -76,6 +76,7 @@ dev = [ "pytest-freezer == 0.4.8", "setuptools-scm == 8.1.0", "tox == 4.15.0", + "types-Pillow == 10.1.0.2", # typing-extensions needed for TypeAlias added in Py 3.10 "typing-extensions == 4.9.0 ; python_version < '3.10'", ] diff --git a/core/src/toga/__init__.py b/core/src/toga/__init__.py index a6fc276daf..391f199554 100644 --- a/core/src/toga/__init__.py +++ b/core/src/toga/__init__.py @@ -1,9 +1,12 @@ +from __future__ import annotations + import warnings +from pathlib import Path from .app import App, DocumentApp, DocumentMainWindow, MainWindow # Resources -from .colors import hsl, hsla, rgb, rgba +from .colors import hsl, hsla, rgb, rgba # type: ignore[attr-defined] from .command import Command, Group from .documents import Document from .fonts import Font @@ -49,7 +52,7 @@ class NotImplementedWarning(RuntimeWarning): # single argument (the warning message). Use a factory method to avoid reproducing # the message format and the warn invocation. @classmethod - def warn(self, platform, feature): + def warn(cls, platform: str, feature: str) -> None: """Raise a warning that a feature isn't implemented on a platform.""" warnings.warn(NotImplementedWarning(f"[{platform}] Not implemented: {feature}")) @@ -114,7 +117,7 @@ def warn(self, platform, feature): ] -def _package_version(file, name): +def _package_version(file: Path | str | None, name: str) -> str: try: # Read version from SCM metadata # This will only exist in a development environment diff --git a/core/src/toga/app.py b/core/src/toga/app.py index c33ac9bfd0..7246cc2166 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -6,32 +6,25 @@ import sys import warnings import webbrowser -from collections.abc import ( - Collection, - ItemsView, - Iterator, - KeysView, - Mapping, - MutableSet, - ValuesView, -) +from collections.abc import Iterator from email.message import Message from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, AbstractSet, Any, MutableSet, Protocol, Union from warnings import warn from weakref import WeakValueDictionary -from toga.command import Command, CommandSet +from toga.command import CommandSet from toga.documents import Document -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.hardware.camera import Camera from toga.hardware.location import Location from toga.icons import Icon from toga.paths import Paths from toga.platform import get_platform_factory from toga.screens import Screen +from toga.types import TypeAlias from toga.widgets.base import Widget -from toga.window import Window +from toga.window import OnCloseHandlerT, Window if TYPE_CHECKING: from toga.icons import IconContent @@ -41,47 +34,68 @@ class AppStartupMethod(Protocol): - def __call__(self, app: App, **kwargs: Any) -> Widget: + def __call__(self, app: App, /) -> Widget: """The startup method of the app. Called during app startup to set the initial main window content. :param app: The app instance that is starting. - :param kwargs: Ensures compatibility with additional arguments introduced in - future versions. :returns: The widget to use as the main window content. """ - ... -class OnExitHandler(Protocol): - def __call__(self, app: App, **kwargs: Any) -> bool: +class OnExitHandlerSync(Protocol): + def __call__(self, app: App, /) -> bool: """A handler to invoke when the app is about to exit. The return value of this callback controls whether the app is allowed to exit. This can be used to prevent the app exiting with unsaved changes, etc. :param app: The app instance that is exiting. - :param kwargs: Ensures compatibility with additional arguments introduced in - future versions. :returns: ``True`` if the app is allowed to exit; ``False`` if the app is not allowed to exit. """ - ... -class BackgroundTask(Protocol): - def __call__(self, app: App, **kwargs: Any) -> None: - """Code that should be executed as a background task. +class OnExitHandlerAsync(Protocol): + async def __call__(self, app: App, /) -> bool: + """Async definition of :any:`OnExitHandlerSync`.""" + + +class OnExitHandlerGenerator(Protocol): + def __call__(self, app: App, /) -> HandlerGeneratorReturnT[bool]: + """Generator definition of :any:`OnExitHandlerSync`.""" + + +OnExitHandlerT: TypeAlias = Union[ + OnExitHandlerSync, OnExitHandlerAsync, OnExitHandlerGenerator +] + + +class BackgroundTaskSync(Protocol): + def __call__(self, app: App, /) -> object: + """Code that will be executed as a background task. :param app: The app that is handling the background task. - :param kwargs: Ensures compatibility with additional arguments introduced in - future versions. """ - ... -class WindowSet(MutableSet): +class BackgroundTaskAsync(Protocol): + async def __call__(self, app: App, /) -> object: + """Async definition of :any:`BackgroundTaskSync`.""" + + +class BackgroundTaskGenerator(Protocol): + def __call__(self, app: App, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`BackgroundTaskSync`.""" + + +BackgroundTaskT: TypeAlias = Union[ + BackgroundTaskSync, BackgroundTaskAsync, BackgroundTaskGenerator +] + + +class WindowSet(MutableSet[Window]): def __init__(self, app: App): """A collection of windows managed by an app. @@ -90,7 +104,7 @@ def __init__(self, app: App): :attr:`~toga.Window.app` property of the Window. """ self.app = app - self.elements = set() + self.elements: set[Window] = set() def add(self, window: Window) -> None: if not isinstance(window, Window): @@ -111,7 +125,7 @@ def discard(self, window: Window) -> None: # 2023-10: Backwards compatibility ###################################################################### - def __iadd__(self, window: Window) -> None: + def __iadd__(self, window: AbstractSet[Any]) -> WindowSet: # The standard set type does not have a += operator. warn( "Windows are automatically associated with the app; += is not required", @@ -120,7 +134,7 @@ def __iadd__(self, window: Window) -> None: ) return self - def __isub__(self, other: Window) -> None: + def __isub__(self, other: AbstractSet[Any]) -> WindowSet: # The standard set type does have a -= operator, but it takes sets rather than # individual items. warn( @@ -134,10 +148,10 @@ def __isub__(self, other: Window) -> None: # End backwards compatibility ###################################################################### - def __iter__(self) -> Iterator: + def __iter__(self) -> Iterator[Window]: return iter(self.elements) - def __contains__(self, value: Window) -> bool: + def __contains__(self, value: object) -> bool: return value in self.elements def __len__(self) -> int: @@ -153,7 +167,7 @@ class WidgetRegistry: # values()) are all proxied to underlying data store. Private methods exist for # internal use, but those methods shouldn't be used by end-users. - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): self._registry = WeakValueDictionary(*args, **kwargs) def __len__(self) -> int: @@ -175,13 +189,13 @@ def __repr__(self) -> str: + "}" ) - def items(self) -> ItemsView: + def items(self) -> Iterator[tuple[str, Widget]]: return self._registry.items() - def keys(self) -> KeysView: + def keys(self) -> Iterator[str]: return self._registry.keys() - def values(self) -> ValuesView: + def values(self) -> Iterator[Widget]: return self._registry.values() # Private methods for internal use @@ -211,8 +225,8 @@ def __init__( resizable: bool = True, minimizable: bool = True, content: Widget | None = None, - resizeable=None, # DEPRECATED - closeable=None, # DEPRECATED + resizeable: None = None, # DEPRECATED + closeable: None = None, # DEPRECATED ): """Create a new main window. @@ -247,7 +261,7 @@ def __init__( def _default_title(self) -> str: return App.app.formal_name - @property + @property # type: ignore[override] def on_close(self) -> None: """The handler to invoke before the window is closed in response to a user action. @@ -260,7 +274,7 @@ def on_close(self) -> None: return None @on_close.setter - def on_close(self, handler: Any): + def on_close(self, handler: OnCloseHandlerT | None) -> None: if handler: raise ValueError( "Cannot set on_close handler for the main window. " @@ -286,7 +300,7 @@ def __init__( (e.g., due to having unsaved change), override :meth:`toga.Document.can_close()`, rather than implementing an on_close handler. - :param document: The document being managed by this window + :param doc: The document being managed by this window :param id: The ID of the window. :param title: Title for the window. Defaults to the formal name of the app. :param position: Position of the window, as a tuple of ``(x, y)`` coordinates. @@ -314,7 +328,9 @@ def _default_title(self) -> str: class App: #: The currently running :class:`~toga.App`. Since there can only be one running #: Toga app in a process, this is available as a class property via ``toga.App.app``. - app: App = None + app: App + _impl: Any + _camera: Camera def __init__( self, @@ -328,9 +344,9 @@ def __init__( home_page: str | None = None, description: str | None = None, startup: AppStartupMethod | None = None, - on_exit: OnExitHandler | None = None, - id=None, # DEPRECATED - windows=None, # DEPRECATED + on_exit: OnExitHandlerT | None = None, + id: None = None, # DEPRECATED + windows: None = None, # DEPRECATED ): """Create a new App instance. @@ -374,7 +390,7 @@ def __init__( # 2023-10: Backwards compatibility ###################################################################### if id is not None: - warn( + warn( # type: ignore[unreachable] "App.id is deprecated and will be ignored. Use app_id instead", DeprecationWarning, stacklevel=2, @@ -434,7 +450,7 @@ def __init__( if formal_name: self._formal_name = formal_name else: - self._formal_name = self.metadata.get("Formal-Name") + self._formal_name = self.metadata.get("Formal-Name") # type: ignore[assignment] if self._formal_name is None: raise RuntimeError("Toga application must have a formal name") @@ -443,30 +459,26 @@ def __init__( if app_id: self._app_id = app_id else: - self._app_id = self.metadata.get("App-ID", None) + self._app_id = self.metadata.get("App-ID") # type: ignore[assignment] if self._app_id is None: raise RuntimeError("Toga application must have an app ID") # Other metadata may be passed to the constructor, or loaded with importlib. - if author: - self._author = author - else: - self._author = self.metadata.get("Author", None) + self._author = author + if not self._author: + self._author = self.metadata.get("Author") - if version: - self._version = version - else: - self._version = self.metadata.get("Version", None) + self._version = version + if not self._version: + self._version = self.metadata.get("Version") - if home_page: - self._home_page = home_page - else: - self._home_page = self.metadata.get("Home-page", None) + self._home_page = home_page + if not self._home_page: + self._home_page = self.metadata.get("Home-page") - if description: - self._description = description - else: - self._description = self.metadata.get("Summary", None) + self._description = description + if not self._description: + self._description = self.metadata.get("Summary") # Get a platform factory. self.factory = get_platform_factory() @@ -479,26 +491,26 @@ def __init__( else: self.icon = icon - self.on_exit = on_exit + self.on_exit = on_exit # type: ignore[assignment] - # We need the command set to exist so that startup et al can add commands; + # We need the command set to exist so that startup et al. can add commands; # but we don't have an impl yet, so we can't set the on_change handler self._commands = CommandSet() self._startup_method = startup - self._main_window = None + self._main_window: MainWindow | None = None self._windows = WindowSet(self) - self._full_screen_windows = None + self._full_screen_windows: tuple[Window, ...] | None = None self._create_impl() # Now that we have an impl, set the on_change handler for commands self.commands.on_change = self._impl.create_menus - def _create_impl(self): - return self.factory.App(interface=self) + def _create_impl(self) -> None: + self.factory.App(interface=self) ###################################################################### # App properties @@ -552,7 +564,8 @@ def icon(self, icon_or_name: IconContent) -> None: if isinstance(icon_or_name, Icon): self._icon = icon_or_name else: - self._icon = Icon(icon_or_name) + # TODO:PR: not valid for icon_or_name to be None + self._icon = Icon(icon_or_name) # type:ignore[arg-type] try: self._impl.set_icon(self._icon) @@ -587,7 +600,7 @@ def is_bundled(self) -> bool: # App lifecycle ###################################################################### - def add_background_task(self, handler: BackgroundTask) -> None: + def add_background_task(self, handler: BackgroundTaskT) -> None: """Schedule a task to run in the background. Schedules a coroutine or a generator to run in the background. Control @@ -627,23 +640,23 @@ def main_loop(self) -> None: self._impl.main_loop() @property - def main_window(self) -> MainWindow: + def main_window(self) -> MainWindow | None: """The main window for the app.""" return self._main_window @main_window.setter - def main_window(self, window: MainWindow) -> None: + def main_window(self, window: MainWindow | None) -> None: self._main_window = window self._impl.set_main_window(window) - def _verify_startup(self): + def _verify_startup(self) -> None: if not isinstance(self.main_window, MainWindow): raise ValueError( "Application does not have a main window. " "Does your startup() method assign a value to self.main_window?" ) - def _startup(self): + def _startup(self) -> None: # This is a wrapper around the user's startup method that performs any # post-setup validation. self.startup() @@ -680,7 +693,7 @@ def camera(self) -> Camera: return self._camera @property - def commands(self) -> MutableSet[Command]: + def commands(self) -> CommandSet: """The commands available in the app.""" return self._commands @@ -712,7 +725,7 @@ def screens(self) -> list[Screen]: return [screen.interface for screen in self._impl.get_screens()] @property - def widgets(self) -> Mapping[str, Widget]: + def widgets(self) -> WidgetRegistry: """The widgets managed by the app, over all windows. Can be used to look up widgets by ID over the entire app (e.g., @@ -725,7 +738,7 @@ def widgets(self) -> Mapping[str, Widget]: return self._widgets @property - def windows(self) -> Collection[Window]: + def windows(self) -> WindowSet: """The windows managed by the app. Windows are automatically added to the app when they are created, and removed when they are closed.""" return self._windows @@ -779,7 +792,7 @@ def current_window(self) -> Window | None: return window.interface @current_window.setter - def current_window(self, window: Window): + def current_window(self, window: Window) -> None: """Set a window into current active focus.""" self._impl.set_current_window(window) @@ -819,13 +832,13 @@ def set_full_screen(self, *windows: Window) -> None: ###################################################################### @property - def on_exit(self) -> OnExitHandler: + def on_exit(self) -> WrappedHandlerT: """The handler to invoke if the user attempts to exit the app.""" return self._on_exit @on_exit.setter - def on_exit(self, handler: OnExitHandler | None) -> None: - def cleanup(app, should_exit): + def on_exit(self, handler: OnExitHandlerT | None) -> None: + def cleanup(app: App, should_exit: bool) -> None: if should_exit or handler is None: app.exit() @@ -846,8 +859,8 @@ def name(self) -> str: return self._formal_name # Support WindowSet __iadd__ and __isub__ - @windows.setter - def windows(self, windows): + @windows.setter # type:ignore[no-redef,attr-defined,misc] + def windows(self, windows: WindowSet) -> None: if windows is not self._windows: raise AttributeError("can't set attribute 'windows'") @@ -869,9 +882,9 @@ def __init__( home_page: str | None = None, description: str | None = None, startup: AppStartupMethod | None = None, - document_types: dict[str, type[Document]] = None, - on_exit: OnExitHandler | None = None, - id=None, # DEPRECATED + document_types: dict[str, type[Document]] | None = None, + on_exit: OnExitHandlerT | None = None, + id: None = None, # DEPRECATED ): """Create a document-based application. @@ -885,7 +898,7 @@ def __init__( raise ValueError("A document must manage at least one document type.") self._document_types = document_types - self._documents = [] + self._documents: list[Document] = [] super().__init__( formal_name=formal_name, @@ -901,10 +914,10 @@ def __init__( id=id, ) - def _create_impl(self): - return self.factory.DocumentApp(interface=self) + def _create_impl(self) -> None: + self.factory.DocumentApp(interface=self) - def _verify_startup(self): + def _verify_startup(self) -> None: # No post-startup validation required for DocumentApps pass @@ -930,7 +943,7 @@ def startup(self) -> None: Subclasses can override this method to define customized startup behavior. """ - def _open(self, path): + def _open(self, path: Path) -> None: """Internal utility method; open a new document in this app, and shows the document. :param path: The path to the document to be opened. @@ -942,6 +955,8 @@ def _open(self, path): except KeyError: raise ValueError(f"Don't know how to open documents of type {path.suffix}") else: - document = DocType(path, app=self) + # TODO:PR: does this need `document_type`? or is `Document` not the right type? + # TODO:PR: revisit this once #2244 is merged + document = DocType(path, app=self) # type: ignore[call-arg] self._documents.append(document) document.show() diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 3963550b03..7cc0759673 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -1,11 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Iterator, Protocol, Union, no_type_check -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.icons import Icon from toga.keys import Key from toga.platform import get_platform_factory +from toga.types import TypeAlias if TYPE_CHECKING: from toga.app import App @@ -40,7 +41,7 @@ def __init__( # Prime the underlying value of _parent so that the setter has a current value # to work with - self._parent = None + self._parent: Group | None = None self.parent = parent @property @@ -49,7 +50,7 @@ def parent(self) -> Group | None: return self._parent @parent.setter - def parent(self, parent: Group | None): + def parent(self, parent: Group | None) -> None: if parent is None: self._parent = None elif parent == self: @@ -97,17 +98,17 @@ def is_child_of(self, parent: Group | None) -> bool: def __hash__(self) -> int: return hash(self.key) - def __lt__(self, other: Any) -> bool: + def __lt__(self, other: object) -> bool: if not isinstance(other, (Group, Command)): return False return self.key < other.key - def __gt__(self, other: Any) -> bool: + def __gt__(self, other: object) -> bool: if not isinstance(other, (Group, Command)): return False return other < self - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, (Group, Command)): return False return self.key == other.key @@ -121,7 +122,7 @@ def __repr__(self) -> str: return f"" @property - def key(self) -> tuple[(int, int, str)]: + def key(self) -> tuple[tuple[int, int, str], ...]: """A unique tuple describing the path to this group.""" self_tuple = (self.section, self.order, self.text) if self.parent is None: @@ -130,13 +131,13 @@ def key(self) -> tuple[(int, int, str)]: # Standard groups - docstrings can only be provided within the `class` statement, # but the objects can't be instantiated here. - APP = None #: Application-level commands - FILE = None #: File commands - EDIT = None #: Editing commands - VIEW = None #: Content appearance commands - COMMANDS = None #: Default group for user-provided commands - WINDOW = None #: Window management commands - HELP = None #: Help commands + APP: Group #: Application-level commands + FILE: Group #: File commands + EDIT: Group #: Editing commands + VIEW: Group #: Content appearance commands + COMMANDS: Group #: Default group for user-provided commands + WINDOW: Group #: Window management commands + HELP: Group #: Help commands Group.APP = Group("*", order=0) @@ -148,21 +149,35 @@ def key(self) -> tuple[(int, int, str)]: Group.HELP = Group("Help", order=100) -class ActionHandler(Protocol): - def __call__(self, command: Command, **kwargs) -> bool: +class ActionHandlerSync(Protocol): + def __call__(self, command: Command, /) -> bool: """A handler that will be invoked when a Command is invoked. :param command: The command that triggered the action. - :param kwargs: Ensures compatibility with additional arguments introduced in - future versions. """ - ... + + +class ActionHandlerAsync(Protocol): + async def __call__(self, command: Command, /) -> bool: + """Async definition of :any:`ActionHandlerSync`.""" + + +class ActionHandlerGenerator(Protocol): + async def __call__(self, command: Command, /) -> HandlerGeneratorReturnT[bool]: + """Generator definition of :any:`ActionHandlerSync`.""" + + +ActionHandlerT: TypeAlias = Union[ + ActionHandlerSync, + ActionHandlerAsync, + ActionHandlerGenerator, +] class Command: def __init__( self, - action: ActionHandler | None, + action: ActionHandlerT | None, text: str, *, shortcut: str | Key | None = None, @@ -197,7 +212,7 @@ def __init__( self.shortcut = shortcut self.tooltip = tooltip - self.icon = icon + self.icon = icon # type: ignore[assignment] self.group = group self.section = section @@ -208,10 +223,11 @@ def __init__( self.factory = get_platform_factory() self._impl = self.factory.Command(interface=self) + self._enabled = True self.enabled = enabled @property - def key(self) -> tuple[(int, int, str)]: + def key(self) -> tuple[tuple[int, int, str], ...]: """A unique tuple describing the path to this command. Each element in the tuple describes the (section, order, text) for the @@ -225,7 +241,7 @@ def enabled(self) -> bool: return self._enabled @enabled.setter - def enabled(self, value: bool): + def enabled(self, value: bool) -> None: self._enabled = value and getattr(self.action, "_raw", True) is not None self._impl.set_enabled(value) @@ -238,36 +254,36 @@ def icon(self) -> Icon | None: return self._icon @icon.setter - def icon(self, icon_or_name: IconContent | None): + def icon(self, icon_or_name: IconContent | None) -> None: if isinstance(icon_or_name, Icon) or icon_or_name is None: self._icon = icon_or_name else: self._icon = Icon(icon_or_name) @property - def action(self) -> ActionHandler | None: + def action(self) -> WrappedHandlerT | None: """The Action attached to the command.""" return self._action @action.setter - def action(self, action: ActionHandler | None): + def action(self, action: ActionHandlerT | None) -> None: """Set the action attached to the command Needs to be a valid ActionHandler or ``None`` """ self._action = wrapped_handler(self, action) - def __lt__(self, other: Any) -> bool: + def __lt__(self, other: object) -> bool: if not isinstance(other, (Group, Command)): return False return self.key < other.key - def __gt__(self, other: Any) -> bool: + def __gt__(self, other: object) -> bool: if not isinstance(other, (Group, Command)): return False return other < self - def __repr__(self) -> bool: + def __repr__(self) -> str: return ( f" bool: class Separator: - def __init__(self, group: Group = None): + def __init__(self, group: Group | None = None): """A representation of a separator between sections in a Group. :param group: The group that contains the separator. @@ -285,24 +301,23 @@ def __init__(self, group: Group = None): self.group = group def __repr__(self) -> str: - return f"" + return f"" - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: if isinstance(other, Separator): return self.group == other.group return False class CommandSetChangeHandler(Protocol): - def __call__(self) -> None: + def __call__(self, /) -> object: """A handler that will be invoked when a Command or Group is added to the CommandSet.""" - ... class CommandSet: def __init__( self, - on_change: CommandSetChangeHandler = None, + on_change: CommandSetChangeHandler | None = None, app: App | None = None, ): """ @@ -318,35 +333,35 @@ def __init__( :param on_change: A method that should be invoked when this command set changes. :param app: The app this command set is associated with, if it is not the app's - own commandset. + own :any:`CommandSet`. """ self._app = app - self._commands = set() + self._commands: set[Command | Group] = set() self.on_change = on_change - def add(self, *commands: Command | Group): - if self.app and self.app is not None: + def add(self, *commands: Command | Group) -> None: + if self.app: self.app.commands.add(*commands) self._commands.update(commands) if self.on_change: self.on_change() - def clear(self): + def clear(self) -> None: self._commands = set() if self.on_change: self.on_change() @property - def app(self) -> App: + def app(self) -> App | None: return self._app def __len__(self) -> int: return len(self._commands) - def __iter__(self) -> Command | Separator: + def __iter__(self) -> Iterator[Command | Separator]: cmd_iter = iter(sorted(self._commands)) - def descendant(group, ancestor): + def descendant(group: Group, ancestor: Group) -> Group | None: # Return the immediate descendant of ancestor used by this group. if group.parent == ancestor: return group @@ -365,6 +380,7 @@ def descendant(group, ancestor): # can't `peek` at the top element of an iterator, `push` an item back on after # it has been consumed, or pass the consumed item as a return value in addition # to the generator result. + @no_type_check def _iter_group(parent): nonlocal command nonlocal finished diff --git a/core/src/toga/constants/__init__.py b/core/src/toga/constants/__init__.py index 1f7f3cf47b..8c87e451d0 100644 --- a/core/src/toga/constants/__init__.py +++ b/core/src/toga/constants/__init__.py @@ -4,13 +4,15 @@ class Direction(Enum): - "The direction a given property should act" + """The direction a given property should act.""" + HORIZONTAL = 0 VERTICAL = 1 class Baseline(Enum): - "The meaning of a Y coordinate when drawing text." + """The meaning of a Y coordinate when drawing text.""" + ALPHABETIC = auto() #: Alphabetic baseline of the first line TOP = auto() #: Top of text MIDDLE = auto() #: Middle of text @@ -18,7 +20,8 @@ class Baseline(Enum): class FillRule(Enum): - "The rule to use when filling paths." + """The rule to use when filling paths.""" + EVENODD = 0 NONZERO = 1 @@ -36,7 +39,7 @@ class FlashMode(Enum): OFF = 0 ON = 1 - def __str__(self): + def __str__(self) -> str: return self.name.title() diff --git a/core/src/toga/documents.py b/core/src/toga/documents.py index a4a02dd7c7..1a30f338d7 100644 --- a/core/src/toga/documents.py +++ b/core/src/toga/documents.py @@ -16,7 +16,7 @@ def __init__( self, path: str | Path, document_type: str, - app: App = None, + app: App, ): """Create a new Document. @@ -27,7 +27,7 @@ def __init__( self._path = Path(path) self._document_type = document_type self._app = app - self._main_window = None + self._main_window: Window | None = None # Create the visual representation of the document self.create() @@ -46,7 +46,7 @@ def can_close(self) -> bool: """ return True - async def handle_close(self, window, **kwargs): + async def handle_close(self, window: Window, **kwargs: object) -> bool: """An ``on-close`` handler for the main window of this document that implements platform-specific document close behavior. @@ -82,7 +82,7 @@ def filename(self) -> Path: return self._path @property - def document_type(self) -> Path: + def document_type(self) -> str: """A human-readable description of the document type (read-only).""" return self._document_type @@ -92,17 +92,18 @@ def app(self) -> App: return self._app @property - def main_window(self) -> Window: + def main_window(self) -> Window | None: """The main window for the document.""" return self._main_window @main_window.setter - def main_window(self, window): + def main_window(self, window: Window) -> None: self._main_window = window def show(self) -> None: """Show the :any:`main_window` for this document.""" - self.main_window.show() + # TODO:PR: does this need a None check for main_window? + self.main_window.show() # type: ignore[union-attr] @abstractmethod def create(self) -> None: diff --git a/core/src/toga/fonts.py b/core/src/toga/fonts.py index bbb1042dce..f2a7016d49 100644 --- a/core/src/toga/fonts.py +++ b/core/src/toga/fonts.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + # Use the Travertino font definitions as-is from travertino import constants from travertino.constants import ( @@ -27,7 +29,7 @@ FONT_STYLES = {NORMAL, ITALIC, OBLIQUE} FONT_VARIANTS = {NORMAL, SMALL_CAPS} -_REGISTERED_FONT_CACHE = {} +_REGISTERED_FONT_CACHE: dict[tuple[str, str, str, str], str] = {} class Font(BaseFont): @@ -68,7 +70,14 @@ def __str__(self) -> str: return f"{self.family} {size}{weight}{variant}{style}" @staticmethod - def register(family, path, *, weight=NORMAL, style=NORMAL, variant=NORMAL): + def register( + family: str, + path: str | Path, + *, + weight: str = NORMAL, + style: str = NORMAL, + variant: str = NORMAL, + ) -> None: """Registers a file-based font. **Note:** This is not currently supported on macOS or iOS. @@ -84,7 +93,12 @@ def register(family, path, *, weight=NORMAL, style=NORMAL, variant=NORMAL): _REGISTERED_FONT_CACHE[font_key] = str(toga.App.app.paths.app / path) @staticmethod - def _registered_font_key(family, weight, style, variant): + def _registered_font_key( + family: str, + weight: str, + style: str, + variant: str, + ) -> tuple[str, str, str, str]: if weight not in constants.FONT_WEIGHTS: weight = NORMAL if style not in constants.FONT_STYLES: diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index c111473ae6..d6e30a84d2 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -1,17 +1,46 @@ +from __future__ import annotations + import asyncio import inspect import sys import traceback import warnings from abc import ABC +from typing import ( + Any, + Awaitable, + Callable, + Generator, + NoReturn, + Protocol, + TypeVar, + Union, +) + +from toga.types import TypeAlias + +GeneratorReturnT = TypeVar("GeneratorReturnT") +HandlerGeneratorReturnT: TypeAlias = Generator[ + Union[float, None], object, GeneratorReturnT +] + +HandlerSyncT: TypeAlias = Callable[..., object] +HandlerAsyncT: TypeAlias = Callable[..., Awaitable[object]] +HandlerGeneratorT: TypeAlias = Callable[..., HandlerGeneratorReturnT[object]] +HandlerT: TypeAlias = Union[HandlerSyncT, HandlerAsyncT, HandlerGeneratorT] +WrappedHandlerT: TypeAlias = Callable[..., object] class NativeHandler: - def __init__(self, handler): + def __init__(self, handler: Callable[..., object]): self.native = handler -async def long_running_task(interface, generator, cleanup): +async def long_running_task( + interface: object, + generator: HandlerGeneratorReturnT[object], + cleanup: HandlerSyncT | None, +) -> None: """Run a generator as an asynchronous coroutine.""" try: try: @@ -33,7 +62,13 @@ async def long_running_task(interface, generator, cleanup): traceback.print_exc() -async def handler_with_cleanup(handler, cleanup, interface, *args, **kwargs): +async def handler_with_cleanup( + handler: HandlerAsyncT, + cleanup: HandlerSyncT | None, + interface: object, + *args: object, + **kwargs: object, +) -> None: try: result = await handler(interface, *args, **kwargs) except Exception as e: @@ -48,7 +83,11 @@ async def handler_with_cleanup(handler, cleanup, interface, *args, **kwargs): traceback.print_exc() -def wrapped_handler(interface, handler, cleanup=None): +def wrapped_handler( + interface: object, + handler: HandlerT | NativeHandler | None, + cleanup: HandlerSyncT | None = None, +) -> WrappedHandlerT: """Wrap a handler provided by the user, so it can be invoked. If the handler is a NativeHandler, return the handler object contained in the @@ -70,7 +109,7 @@ def wrapped_handler(interface, handler, cleanup=None): if isinstance(handler, NativeHandler): return handler.native - def _handler(*args, **kwargs): + def _handler(*args: object, **kwargs: object) -> object: if asyncio.iscoroutinefunction(handler): asyncio.ensure_future( handler_with_cleanup(handler, cleanup, interface, *args, **kwargs) @@ -94,35 +133,47 @@ def _handler(*args, **kwargs): except Exception as e: print("Error in handler cleanup:", e, file=sys.stderr) traceback.print_exc() + return None - _handler._raw = handler + _handler._raw = handler # type: ignore[attr-defined] else: # A dummy no-op handler - def _handler(*args, **kwargs): + def _handler(*args: object, **kwargs: object) -> object: try: if cleanup: cleanup(interface, None) except Exception as e: print("Error in handler cleanup:", e, file=sys.stderr) traceback.print_exc() + return None - _handler._raw = None + _handler._raw = None # type: ignore[attr-defined] return _handler +class OnResultT(Protocol): + def __call__( + self, result: Any, /, exception: Exception | None = None + ) -> object: ... + + class AsyncResult(ABC): - def __init__(self, on_result=None): + RESULT_TYPE: str + + def __init__(self, on_result: OnResultT | None = None) -> None: loop = asyncio.get_event_loop() self.future = loop.create_future() ###################################################################### # 2023-12: Backwards compatibility ###################################################################### + self.on_result: OnResultT | None if on_result: warnings.warn( - "Synchronous `on_result` handlers have been deprecated; use `await` on the asynchronous result", + "Synchronous `on_result` handlers have been deprecated; " + "use `await` on the asynchronous result", DeprecationWarning, ) @@ -133,26 +184,26 @@ def __init__(self, on_result=None): # End backwards compatibility. ###################################################################### - def set_result(self, result): + def set_result(self, result: Any) -> None: if not self.future.cancelled(): self.future.set_result(result) if self.on_result: self.on_result(result) - def set_exception(self, exc): + def set_exception(self, exc: Exception) -> None: if not self.future.cancelled(): self.future.set_exception(exc) if self.on_result: self.on_result(None, exception=exc) - def __repr__(self): + def __repr__(self) -> str: return f"" - def __await__(self): + def __await__(self) -> Generator[Any, None, Any]: return self.future.__await__() # All the comparison dunder methods are disabled - def __bool__(self, other): + def __bool__(self, other: object) -> NoReturn: raise RuntimeError( f"Can't check {self.RESULT_TYPE} result directly; use await or an on_result handler" ) diff --git a/core/src/toga/hardware/camera.py b/core/src/toga/hardware/camera.py index 08d4ec430e..91e3fd5c6b 100644 --- a/core/src/toga/hardware/camera.py +++ b/core/src/toga/hardware/camera.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from toga.constants import FlashMode from toga.handlers import AsyncResult, PermissionResult @@ -15,7 +15,7 @@ class PhotoResult(AsyncResult): class CameraDevice: - def __init__(self, impl): + def __init__(self, impl: Any): self._impl = impl @property @@ -33,8 +33,8 @@ def has_flash(self) -> bool: """Does the device have a flash?""" return self._impl.has_flash() - def __eq__(self, other) -> bool: - return self.id == other.id + def __eq__(self, other: object) -> bool: + return self.id == other.id # type:ignore[attr-defined] def __repr__(self) -> str: return f"" diff --git a/core/src/toga/icons.py b/core/src/toga/icons.py index 729ae4c09e..db8ddc7cfa 100644 --- a/core/src/toga/icons.py +++ b/core/src/toga/icons.py @@ -1,34 +1,26 @@ from __future__ import annotations -import sys import warnings from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Iterable, Union import toga from toga.platform import get_platform_factory +from toga.types import TypeAlias if TYPE_CHECKING: - if sys.version_info < (3, 10): - from typing_extensions import TypeAlias - else: - from typing import TypeAlias - - IconContent: TypeAlias = str | Path | toga.Icon + IconContent: TypeAlias = Union[str, Path, toga.Icon] class cachedicon: - def __init__(self, f): + def __init__(self, f: Callable[..., Icon]): self.f = f self.__doc__ = f.__doc__ - def __get__(self, obj, owner): + def __get__(self, obj: object, owner: type[Icon]) -> Icon: # If you ask for Icon.CACHED_ICON, obj is None, and owner is the Icon class # If you ask for self.CACHED_ICON, obj is self, from which we can get the class. - if obj is None: - cls = owner - else: - cls = obj.__class__ + cls = owner if obj is None else obj.__class__ try: # Look for a __CACHED_ICON attribute on the class @@ -107,10 +99,14 @@ def __init__( self.system = system if self.system: - resource_path = Path(self.factory.__file__).parent / "resources" + resource_path = ( + Path(self.factory.__file__).parent # type:ignore[arg-type] + / "resources" + ) else: resource_path = toga.App.app.paths.app + full_path: dict[str, Path] | Path if self.factory.Icon.SIZES: full_path = {} for size in self.factory.Icon.SIZES: @@ -154,7 +150,12 @@ def __init__( ) self._impl = self.DEFAULT_ICON._impl - def _full_path(self, size, extensions, resource_path): + def _full_path( + self, + size: str | None, + extensions: Iterable[str], + resource_path: Path, + ) -> Path: platform = toga.platform.current_platform if size: for extension in extensions: diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 0d374ce73c..e91dc44ba5 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -5,11 +5,12 @@ import warnings from functools import lru_cache from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Any, Protocol, Union from warnings import warn import toga from toga.platform import get_platform_factory +from toga.types import TypeAlias, TypeVar if sys.version_info >= (3, 10): from importlib.metadata import entry_points @@ -22,22 +23,17 @@ warnings.filterwarnings("default", category=DeprecationWarning) if TYPE_CHECKING: - if sys.version_info < (3, 10): - from typing_extensions import TypeAlias, TypeVar - else: - from typing import TypeAlias, TypeVar - # Define a type variable for generics where an Image type is required. ImageT = TypeVar("ImageT") # Define the types that can be used as Image content - PathLike: TypeAlias = str | Path - BytesLike: TypeAlias = bytes | bytearray | memoryview + PathLike: TypeAlias = Union[str, Path] + BytesLike: TypeAlias = Union[bytes, bytearray, memoryview] ImageLike: TypeAlias = Any - ImageContent: TypeAlias = PathLike | BytesLike | ImageLike + ImageContent: TypeAlias = Union[PathLike, BytesLike, ImageLike] # Define a type variable representing an image of an externally defined type. - ExternalImageT = TypeVar("ExternalImageT") + ExternalImageT = TypeVar("ExternalImageT", bound=object) class ImageConverter(Protocol): @@ -45,8 +41,9 @@ class ImageConverter(Protocol): :any:`toga.Image`. """ + # TODO:PR: figure out how to resolve mypy issues #: The base image class this plugin can interpret. - image_class: type[ExternalImageT] + image_class: type[ExternalImageT] # type:ignore[valid-type] @staticmethod def convert_from_format(image_in_format: ExternalImageT) -> BytesLike: @@ -58,7 +55,6 @@ def convert_from_format(image_in_format: ExternalImageT) -> BytesLike: :param image_in_format: An instance of :any:`image_class` (or a subclass). :returns: The image data, in a :ref:`known image format `. """ - ... @staticmethod def convert_to_format( @@ -76,7 +72,6 @@ def convert_to_format( :param image_class: The class of image to return. :returns: The image, as an instance of the image class specified. """ - ... NOT_PROVIDED = object() @@ -87,8 +82,8 @@ def __init__( self, src: ImageContent = NOT_PROVIDED, *, - path=NOT_PROVIDED, # DEPRECATED - data=NOT_PROVIDED, # DEPRECATED + path: object = NOT_PROVIDED, # DEPRECATED + data: object = NOT_PROVIDED, # DEPRECATED ): """Create a new image. @@ -157,7 +152,7 @@ def __init__( @classmethod @lru_cache(maxsize=None) - def _converters(cls): + def _converters(cls) -> list[ImageConverter]: """Return list of registered image plugin converters. Only loaded once.""" converters = [] @@ -172,9 +167,9 @@ def _converters(cls): return converters @property - def size(self) -> (int, int): + def size(self) -> tuple[int, int]: """The size of the image, as a (width, height) tuple.""" - return (self._impl.get_width(), self._impl.get_height()) + return self._impl.get_width(), self._impl.get_height() @property def width(self) -> int: @@ -218,7 +213,7 @@ def as_format(self, format: type[ImageT]) -> ImageT: """ if isinstance(format, type): if issubclass(format, Image): - return format(self.data) + return format(self.data) # type:ignore[return-value] for converter in self._converters(): if issubclass(format, converter.image_class): diff --git a/core/src/toga/keys.py b/core/src/toga/keys.py index bbaa8c1942..efc1c20046 100644 --- a/core/src/toga/keys.py +++ b/core/src/toga/keys.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from enum import Enum @@ -158,7 +160,7 @@ def is_printable(self) -> bool: """Does pressing the key result in a printable character?""" return not (self.value.startswith("<") and self.value.endswith(">")) - def __add__(self, other): + def __add__(self, other: Key | str) -> str: """Allow two Keys to be concatenated, or a string to be concatenated to a Key. Produces a single string definition. @@ -169,10 +171,10 @@ def __add__(self, other): """ try: # Try Key + Key - return self.value + other.value + return self.value + other.value # type: ignore[union-attr] except AttributeError: return self.value + other - def __radd__(self, other): + def __radd__(self, other: str) -> str: """Same as add.""" return other + self.value diff --git a/core/src/toga/paths.py b/core/src/toga/paths.py index a8c38301dd..b82bc28d41 100644 --- a/core/src/toga/paths.py +++ b/core/src/toga/paths.py @@ -1,4 +1,4 @@ -import importlib +import importlib.util import sys from pathlib import Path @@ -7,7 +7,7 @@ class Paths: - def __init__(self): + def __init__(self) -> None: self.factory = get_platform_factory() self._impl = self.factory.Paths(self) @@ -28,7 +28,7 @@ def app(self) -> Path: files into this path. """ try: - return Path(importlib.util.find_spec(toga.App.app.__module__).origin).parent + return Path(importlib.util.find_spec(toga.App.app.__module__).origin).parent # type: ignore except ValueError: # When running a single file `python path/to/myapp.py`, the app # won't have a module because it's the mainline. Default to the diff --git a/core/src/toga/platform.py b/core/src/toga/platform.py index 3c319385d8..2b20eae696 100644 --- a/core/src/toga/platform.py +++ b/core/src/toga/platform.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import importlib import os import sys from functools import lru_cache +from types import ModuleType -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 10): # pragma: no cover from importlib.metadata import entry_points -else: +else: # pragma: no cover # Before Python 3.10, entry_points did not support the group argument; # so, the backport package must be used on older versions. from importlib_metadata import entry_points @@ -26,7 +29,7 @@ } -def get_current_platform(): +def get_current_platform() -> str | None: # Rely on `sys.getandroidapilevel`, which only exists on Android; see # https://github.com/beeware/Python-Android-support/issues/8 if hasattr(sys, "getandroidapilevel"): @@ -49,7 +52,7 @@ def find_backends(): @lru_cache(maxsize=1) -def get_platform_factory(): +def get_platform_factory() -> ModuleType: """Determine the current host platform and import the platform factory. If the ``TOGA_BACKEND`` environment variable is set, the factory will be loaded diff --git a/core/src/toga/plugins/image_formats.py b/core/src/toga/plugins/image_formats.py index 40822049d4..4bd5778397 100644 --- a/core/src/toga/plugins/image_formats.py +++ b/core/src/toga/plugins/image_formats.py @@ -30,7 +30,7 @@ def convert_from_format(image_in_format: PIL.Image.Image) -> bytes: @staticmethod def convert_to_format( data: BytesLike, - image_class: type(PIL.Image.Image), + image_class: type[PIL.Image.Image], ) -> PIL.Image.Image: # PIL Images aren't designed to be subclassed, so no implementation is necessary # for a supplied format. diff --git a/core/src/toga/screens.py b/core/src/toga/screens.py index 65f341cc59..b13411c7d4 100644 --- a/core/src/toga/screens.py +++ b/core/src/toga/screens.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from toga.images import Image from toga.platform import get_platform_factory @@ -10,7 +10,7 @@ class Screen: - def __init__(self, _impl): + def __init__(self, _impl: Any): self._impl = _impl self.factory = get_platform_factory() @@ -29,7 +29,7 @@ def size(self) -> tuple[int, int]: """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() - def as_image(self, format: type[ImageT] = Image) -> ImageT: + def as_image(self, format: type[ImageT] = Image) -> ImageT: # type: ignore[assignment] """Render the current contents of the screen as an image. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also diff --git a/core/src/toga/sources/__init__.py b/core/src/toga/sources/__init__.py index c0d20e7308..c12ec189f1 100644 --- a/core/src/toga/sources/__init__.py +++ b/core/src/toga/sources/__init__.py @@ -3,3 +3,14 @@ from .list_source import ListSource, Row # noqa: F401 from .tree_source import Node, TreeSource # noqa: F401 from .value_source import ValueSource # noqa: F401 + +__all__ = [ + "ListSource", + "Listener", + "Node", + "Row", + "Source", + "TreeSource", + "ValueSource", + "to_accessor", +] diff --git a/core/src/toga/sources/accessors.py b/core/src/toga/sources/accessors.py index c080ec8b95..1e73ae9612 100644 --- a/core/src/toga/sources/accessors.py +++ b/core/src/toga/sources/accessors.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +from typing import Collection, Mapping NON_ACCESSOR_CHARS = re.compile(r"[^\w ]") WHITESPACE = re.compile(r"\s+") @@ -45,8 +46,8 @@ def to_accessor(heading: str) -> str: def build_accessors( - headings: list[str], - accessors: list[str | None] | dict[str, str] | None, + headings: Collection[str], + accessors: Collection[str | None] | Mapping[str, str] | None, ) -> list[str]: """Convert a list of headings (with accessor overrides) to a finalised list of accessors. diff --git a/core/src/toga/sources/base.py b/core/src/toga/sources/base.py index 06e9d539c0..4f730e0f84 100644 --- a/core/src/toga/sources/base.py +++ b/core/src/toga/sources/base.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Protocol +from typing import Any, Protocol class Listener(Protocol): @@ -8,35 +8,35 @@ class Listener(Protocol): data source. """ - def change(self, item): + def change(self, item: Any) -> None: """A change has occurred in an item. :param item: The data object that has changed. """ - def insert(self, index: int, item): + def insert(self, index: int, item: Any) -> None: """An item has been added to the data source. :param index: The 0-index position in the data. :param item: The data object that was added. """ - def remove(self, index: int, item): + def remove(self, index: int, item: Any) -> None: """An item has been removed from the data source. :param index: The 0-index position in the data. :param item: The data object that was added. """ - def clear(self): + def clear(self) -> None: """All items have been removed from the data source.""" class Source: """A base class for data sources, providing an implementation of data notifications.""" - def __init__(self): - self._listeners = [] + def __init__(self) -> None: + self._listeners: list[Listener] = [] @property def listeners(self) -> list[Listener]: @@ -46,7 +46,7 @@ def listeners(self) -> list[Listener]: """ return self._listeners - def add_listener(self, listener: Listener): + def add_listener(self, listener: Listener) -> None: """Add a new listener to this data source. If the listener is already registered on this data source, the @@ -57,14 +57,14 @@ def add_listener(self, listener: Listener): if listener not in self._listeners: self._listeners.append(listener) - def remove_listener(self, listener: Listener): + def remove_listener(self, listener: Listener) -> None: """Remove a listener from this data source. :param listener: The listener to remove. """ self._listeners.remove(listener) - def notify(self, notification: str, **kwargs): + def notify(self, notification: str, **kwargs: object) -> None: """Notify all listeners an event has occurred. :param notification: The notification to emit. diff --git a/core/src/toga/sources/list_source.py b/core/src/toga/sources/list_source.py index b72e696fb9..2119992aae 100644 --- a/core/src/toga/sources/list_source.py +++ b/core/src/toga/sources/list_source.py @@ -1,12 +1,20 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any +from typing import Generic, TypeVar from .base import Source +T = TypeVar("T") -def _find_item(candidates: list, data: Any, accessors: list[str], start, error: str): + +def _find_item( + candidates: list[T], + data: object, + accessors: list[str], + start: T | None, + error: str, +) -> T: """Find-by-value implementation helper; find an item matching ``data`` in ``candidates``, starting with item ``start``.""" if start is not None: @@ -36,8 +44,8 @@ def _find_item(candidates: list, data: Any, accessors: list[str], start, error: raise ValueError(error) -class Row: - def __init__(self, **data): +class Row(Generic[T]): + def __init__(self, **data: T): """Create a new Row object. The keyword arguments specified in the constructor will be converted into @@ -51,7 +59,7 @@ def __init__(self, **data): for name, value in data.items(): setattr(self, name, value) - def __repr__(self): + def __repr__(self) -> str: descriptor = " ".join( f"{attr}={getattr(self, attr)!r}" for attr in sorted(self.__dict__) @@ -63,7 +71,7 @@ def __repr__(self): # Utility wrappers ###################################################################### - def __setattr__(self, attr: str, value): + def __setattr__(self, attr: str, value: T) -> None: """Set an attribute on the Row object, notifying the source of the change. :param attr: The attribute to change. @@ -74,11 +82,10 @@ def __setattr__(self, attr: str, value): if self._source is not None: self._source.notify("change", item=self) - def __delattr__(self, attr: str): + def __delattr__(self, attr: str) -> None: """Remove an attribute from the Row object, notifying the source of the change. :param attr: The attribute to change. - :param value: The new attribute value. """ super().__delattr__(attr) if not attr.startswith("_"): @@ -86,8 +93,9 @@ def __delattr__(self, attr: str): self._source.notify("change", item=self) -class ListSource(Source): - def __init__(self, accessors: list[str], data: Iterable | None = None): +# TODO:PR: consider adding supported Protocols...maybe List? +class ListSource(Source, Generic[T]): + def __init__(self, accessors: Iterable[str], data: Iterable[T] | None = None): """A data source to store an ordered list of multiple data values. :param accessors: A list of attribute names for accessing the value @@ -118,11 +126,11 @@ def __len__(self) -> int: """Returns the number of items in the list.""" return len(self._data) - def __getitem__(self, index: int) -> Row: + def __getitem__(self, index: int) -> Row[T]: """Returns the item at position ``index`` of the list.""" return self._data[index] - def __delitem__(self, index: int): + def __delitem__(self, index: int) -> None: """Deletes the item at position ``index`` of the list.""" row = self._data[index] del self._data[index] @@ -133,7 +141,7 @@ def __delitem__(self, index: int): ###################################################################### # This behavior is documented in list_source.rst. - def _create_row(self, data: Any) -> Row: + def _create_row(self, data: T) -> Row[T]: if isinstance(data, dict): row = Row(**data) elif hasattr(data, "__iter__") and not isinstance(data, str): @@ -147,7 +155,7 @@ def _create_row(self, data: Any) -> Row: # Utility methods to make ListSources more list-like ###################################################################### - def __setitem__(self, index: int, value: Any): + def __setitem__(self, index: int, value: T) -> None: """Set the value of a specific item in the data source. :param index: The item to change @@ -158,12 +166,12 @@ def __setitem__(self, index: int, value: Any): self._data[index] = row self.notify("insert", index=index, item=row) - def clear(self): + def clear(self) -> None: """Clear all data from the data source.""" self._data = [] self.notify("clear") - def insert(self, index: int, data: Any): + def insert(self, index: int, data: T) -> Row[T]: """Insert a row into the data source at a specific index. :param index: The index at which to insert the item. @@ -176,7 +184,7 @@ def insert(self, index: int, data: Any): self.notify("insert", index=index, item=row) return row - def append(self, data): + def append(self, data: T) -> Row[T]: """Insert a row at the end of the data source. :param data: The data to append to the ListSource. This data will be converted @@ -185,14 +193,14 @@ def append(self, data): """ return self.insert(len(self), data) - def remove(self, row: Row): + def remove(self, row: Row[T]) -> None: """Remove a row from the data source. :param row: The row to remove from the data source. """ del self[self._data.index(row)] - def index(self, row: Row) -> int: + def index(self, row: Row[T]) -> int: """The index of a specific row in the data source. This search uses Row instances, and searches for an *instance* match. @@ -206,7 +214,7 @@ def index(self, row: Row) -> int: """ return self._data.index(row) - def find(self, data: Any, start: None | None = None): + def find(self, data: object, start: Row[T] | None = None) -> Row[T]: """Find the first item in the data that matches all the provided attributes. diff --git a/core/src/toga/sources/tree_source.py b/core/src/toga/sources/tree_source.py index 8b4a5268f5..fc569f3dc4 100644 --- a/core/src/toga/sources/tree_source.py +++ b/core/src/toga/sources/tree_source.py @@ -1,13 +1,19 @@ from __future__ import annotations -from typing import Any +from typing import Generic, Iterable, Iterator, Mapping, Tuple, TypeVar, Union + +from toga.types import TypeAlias from .base import Source from .list_source import Row, _find_item +T = TypeVar("T") + + +class Node(Row[T]): + _source: TreeSource[T] -class Node(Row): - def __init__(self, **data): + def __init__(self, **data: T): """Create a new Node object. The keyword arguments specified in the constructor will be converted into @@ -21,10 +27,10 @@ def __init__(self, **data): notified. """ super().__init__(**data) - self._children: list[Node] | None = None - self._parent: Node | None = None + self._children: list[Node[T]] | None = None + self._parent: Node[T] | None = None - def __repr__(self): + def __repr__(self) -> str: descriptor = " ".join( f"{attr}={getattr(self, attr)!r}" for attr in sorted(self.__dict__) @@ -41,13 +47,13 @@ def __repr__(self): # Methods required by the TreeSource interface ###################################################################### - def __getitem__(self, index: int) -> Node: + def __getitem__(self, index: int) -> Node[T]: if self._children is None: raise ValueError(f"{self} is a leaf node") return self._children[index] - def __delitem__(self, index: int): + def __delitem__(self, index: int) -> None: if self._children is None: raise ValueError(f"{self} is a leaf node") @@ -56,12 +62,13 @@ def __delitem__(self, index: int): # Child isn't part of this source, or a child of this node anymore. child._parent = None - child._source = None + # TODO:PR: consider del? + child._source = None # type: ignore[assignment] self._source.notify("remove", parent=self, index=index, item=child) def __len__(self) -> int: - if self.can_have_children(): + if self._children is not None: return len(self._children) else: return 0 @@ -79,10 +86,10 @@ def can_have_children(self) -> bool: # Utility methods to make TreeSource more list-like ###################################################################### - def __iter__(self): + def __iter__(self) -> Iterator[Node[T]]: return iter(self._children or []) - def __setitem__(self, index: int, data: Any): + def __setitem__(self, index: int, data: T) -> None: """Set the value of a specific child in the Node. :param index: The index of the child to change @@ -94,13 +101,14 @@ def __setitem__(self, index: int, data: Any): old_node = self._children[index] old_node._parent = None - old_node._source = None + # TODO:PR: consider del? + old_node._source = None # type: ignore[assignment] node = self._source._create_node(parent=self, data=data) self._children[index] = node self._source.notify("change", item=node) - def insert(self, index: int, data: Any, children: Any = None): + def insert(self, index: int, data: object, children: object = None) -> Node[T]: """Insert a node as a child of this node a specific index. :param index: The index at which to insert the new child. @@ -122,7 +130,7 @@ def insert(self, index: int, data: Any, children: Any = None): self._source.notify("insert", parent=self, index=index, item=node) return node - def append(self, data: Any, children: Any = None): + def append(self, data: object, children: object = None) -> Node[T]: """Append a node to the end of the list of children of this node. :param data: The data to append as a child of this node. This data will be @@ -132,7 +140,7 @@ def append(self, data: Any, children: Any = None): """ return self.insert(len(self), data=data, children=children) - def remove(self, child: Node): + def remove(self, child: Node[T]) -> None: """Remove a child node from this node. :param child: The child node to remove from this node. @@ -140,7 +148,7 @@ def remove(self, child: Node): # Index will raise ValueError if the node is a leaf del self[self.index(child)] - def index(self, child: Node): + def index(self, child: Node[T]) -> int: """The index of a specific node in children of this node. This search uses Node instances, and searches for an *instance* match. @@ -157,7 +165,7 @@ def index(self, child: Node): return self._children.index(child) - def find(self, data: Any, start: Node = None): + def find(self, data: object, start: Node[T] | None = None) -> Node[T]: """Find the first item in the child nodes of this node that matches all the provided attributes. @@ -188,8 +196,20 @@ def find(self, data: Any, start: Node = None): ) -class TreeSource(Source): - def __init__(self, accessors: list[str], data: dict | list[tuple] | None = None): +NodeDataT: TypeAlias = Union[object, Mapping[str, T], Iterable[T]] +TreeSourceDataT: TypeAlias = Union[ + object, + Mapping[NodeDataT[T], "TreeSourceDataT[T]"], + Iterable[Tuple[NodeDataT[T], "TreeSourceDataT[T]"]], +] + + +class TreeSource(Source, Generic[T]): + def __init__( + self, + accessors: Iterable[str], + data: TreeSourceDataT[T] | None = None, + ): super().__init__() if isinstance(accessors, str) or not hasattr(accessors, "__iter__"): raise ValueError("accessors should be a list of attribute names") @@ -200,7 +220,7 @@ def __init__(self, accessors: list[str], data: dict | list[tuple] | None = None) raise ValueError("TreeSource must be provided a list of accessors") if data: - self._roots = self._create_nodes(parent=None, value=data) + self._roots: list[Node[T]] = self._create_nodes(parent=None, value=data) else: self._roots = [] @@ -211,13 +231,14 @@ def __init__(self, accessors: list[str], data: dict | list[tuple] | None = None) def __len__(self) -> int: return len(self._roots) - def __getitem__(self, index: int) -> Node: + def __getitem__(self, index: int) -> Node[T]: return self._roots[index] - def __delitem__(self, index: int): + def __delitem__(self, index: int) -> None: node = self._roots[index] del self._roots[index] - node._source = None + # TODO:PR: consider del? + node._source = None # type: ignore[assignment] self.notify("remove", parent=None, index=index, item=node) ###################################################################### @@ -226,10 +247,10 @@ def __delitem__(self, index: int): def _create_node( self, - parent: Node | None, - data: Any, - children: list | dict | None = None, - ): + parent: Node[T] | None, + data: NodeDataT[T], + children: TreeSourceDataT[T] | None = None, + ) -> Node[T]: if isinstance(data, dict): node = Node(**data) elif hasattr(data, "__iter__") and not isinstance(data, str): @@ -245,7 +266,11 @@ def _create_node( return node - def _create_nodes(self, parent: Node | None, value: Any): + def _create_nodes( + self, + parent: Node[T] | None, + value: TreeSourceDataT[T], + ) -> list[Node[T]]: if isinstance(value, dict): return [ self._create_node(parent=parent, data=data, children=children) @@ -263,7 +288,7 @@ def _create_nodes(self, parent: Node | None, value: Any): # Utility methods to make TreeSources more list-like ###################################################################### - def __setitem__(self, index: int, data: Any): + def __setitem__(self, index: int, data: object) -> None: """Set the value of a specific root item in the data source. :param index: The root item to change @@ -272,13 +297,14 @@ def __setitem__(self, index: int, data: Any): """ old_root = self._roots[index] old_root._parent = None - old_root._source = None + # TODO:PR: consider del? + old_root._source = None # type: ignore[assignment] root = self._create_node(parent=None, data=data) self._roots[index] = root self.notify("change", item=root) - def clear(self): + def clear(self) -> None: """Clear all data from the data source.""" self._roots = [] self.notify("clear") @@ -286,9 +312,9 @@ def clear(self): def insert( self, index: int, - data: Any, - children: Any = None, - ): + data: NodeDataT[T], + children: TreeSourceDataT[T] = None, + ) -> Node[T]: """Insert a root node into the data source at a specific index. If the node is a leaf node, it will be converted into a non-leaf node. @@ -311,7 +337,11 @@ def insert( self.notify("insert", parent=None, index=index, item=node) return node - def append(self, data: Any, children: Any = None): + def append( + self, + data: NodeDataT[T], + children: TreeSourceDataT[T] | None = None, + ) -> Node[T]: """Append a root node at the end of the list of children of this source. If the node is a leaf node, it will be converted into a non-leaf node. @@ -324,7 +354,7 @@ def append(self, data: Any, children: Any = None): """ return self.insert(len(self), data=data, children=children) - def remove(self, node: Node): + def remove(self, node: Node[T]) -> None: """Remove a node from the data source. This will also remove the node if it is a descendant of a root node. @@ -339,7 +369,7 @@ def remove(self, node: Node): else: node._parent.remove(node) - def index(self, node: Node) -> int: + def index(self, node: Node[T]) -> int: """The index of a specific root node in the data source. This search uses Node instances, and searches for an *instance* match. @@ -353,7 +383,7 @@ def index(self, node: Node) -> int: """ return self._roots.index(node) - def find(self, data: Any, start: Node = None): + def find(self, data: NodeDataT[T], start: Node[T] | None = None) -> Node[T]: """Find the first item in the child nodes of the given node that matches all the provided attributes. diff --git a/core/src/toga/sources/value_source.py b/core/src/toga/sources/value_source.py index c77fcf5d3a..1d8f10178b 100644 --- a/core/src/toga/sources/value_source.py +++ b/core/src/toga/sources/value_source.py @@ -4,15 +4,15 @@ class ValueSource(Source): - def __init__(self, value=None, accessor="value"): + def __init__(self, value: object = None, accessor: str = "value"): super().__init__() self.accessor = accessor setattr(self, accessor, value) - def __str__(self): + def __str__(self) -> str: return str(getattr(self, self.accessor, None)) - def __setattr__(self, attr, value): + def __setattr__(self, attr: str, value: object) -> None: super().__setattr__(attr, value) if attr == getattr(self, "accessor", None): self.notify("change", item=value) diff --git a/core/src/toga/style/__init__.py b/core/src/toga/style/__init__.py index 4605b72b2f..33014fdd05 100644 --- a/core/src/toga/style/__init__.py +++ b/core/src/toga/style/__init__.py @@ -1,2 +1,7 @@ from toga.style.applicator import TogaApplicator # noqa: F401 from toga.style.pack import Pack # noqa: F401 + +__all__ = [ + "Pack", + "TogaApplicator", +] diff --git a/core/src/toga/style/applicator.py b/core/src/toga/style/applicator.py index 4f9a7552db..2b0c17ee75 100644 --- a/core/src/toga/style/applicator.py +++ b/core/src/toga/style/applicator.py @@ -1,14 +1,22 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from toga.widgets.base import Widget + + class TogaApplicator: """Apply styles to a Toga widget.""" - def __init__(self, widget): + def __init__(self, widget: Widget): self.widget = widget - def refresh(self): + def refresh(self) -> None: # print("RE-EVALUATE LAYOUT", self.widget) self.widget.refresh() - def set_bounds(self): + def set_bounds(self) -> None: # print(" APPLY LAYOUT", self.widget, self.widget.layout) self.widget._impl.set_bounds( self.widget.layout.absolute_content_left, @@ -19,10 +27,10 @@ def set_bounds(self): for child in self.widget.children: child.applicator.set_bounds() - def set_text_alignment(self, alignment): + def set_text_alignment(self, alignment: str) -> None: self.widget._impl.set_alignment(alignment) - def set_hidden(self, hidden): + def set_hidden(self, hidden: bool) -> None: 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 @@ -38,14 +46,14 @@ def set_hidden(self, hidden): # False False False child.applicator.set_hidden(hidden or child.style._hidden) - def set_font(self, font): + 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() - def set_color(self, color): + def set_color(self, color: object) -> None: self.widget._impl.set_color(color) - def set_background_color(self, color): + def set_background_color(self, color: object) -> None: self.widget._impl.set_background_color(color) diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 455eb2e70f..8978ba79af 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import Any + from travertino.constants import ( # noqa: F401 BOLD, BOTTOM, @@ -11,7 +15,7 @@ LEFT, LTR, MONOSPACE, - NONE, + NONE as NONE, NORMAL, OBLIQUE, RIGHT, @@ -27,6 +31,7 @@ ) from travertino.declaration import BaseStyle, Choices from travertino.layout import BaseBox +from travertino.node import Node from travertino.size import BaseIntrinsicSize from toga.fonts import ( @@ -80,15 +85,15 @@ class IntrinsicSize(BaseIntrinsicSize): _depth = -1 - def _debug(self, *args): # pragma: no cover + def _debug(self, *args: str) -> None: # pragma: no cover print(" " * self.__class__._depth, *args) @property - def _hidden(self): - """Does this style declaration define an object that should be hidden""" + def _hidden(self) -> bool: + """Does this style declaration define an object that should be hidden.""" return self.visibility == HIDDEN - def apply(self, prop, value): + def apply(self, prop: str, value: object) -> None: if self._applicator: if prop == "text_align": if value is None: @@ -127,7 +132,7 @@ def apply(self, prop, value): # so perform a refresh. self._applicator.refresh() - def layout(self, node, viewport): + def layout(self, node: Node, viewport: Any) -> None: # self._debug("=" * 80) # self._debug(f"Layout root {node}, available {viewport.width}x{viewport.height}") self.__class__._depth = -1 @@ -147,12 +152,12 @@ def layout(self, node, viewport): def _layout_node( self, - node, - alloc_width, - alloc_height, - use_all_width, - use_all_height, - ): + node: Node, + alloc_width: int, + alloc_height: int, + use_all_width: bool, + use_all_height: bool, + ) -> None: self.__class__._depth += 1 # self._debug( # f"COMPUTE LAYOUT for {node} available " @@ -259,12 +264,12 @@ def _layout_node( def _layout_row_children( self, - node, - available_width, - available_height, - use_all_width, - use_all_height, - ): + node: Node, + available_width: int, + available_height: int, + use_all_width: bool, + use_all_height: bool, + ) -> tuple[int, int, int, int]: # self._debug(f"LAYOUT ROW CHILDREN {available_width=} {available_height=}") # Pass 1: Lay out all children with a hard-specified width, or an # intrinsic non-flexible width. While iterating, collect the flex @@ -543,12 +548,12 @@ def _layout_row_children( def _layout_column_children( self, - node, - available_width, - available_height, - use_all_width, - use_all_height, - ): + node: Node, + available_width: int, + available_height: int, + use_all_width: bool, + use_all_height: bool, + ) -> tuple[int, int, int, int]: # self._debug(f"LAYOUT COLUMN CHILDREN {available_width=} {available_height=}") # Pass 1: Lay out all children with a hard-specified height, or an # intrinsic non-flexible height. While iterating, collect the flex @@ -819,7 +824,7 @@ def _layout_column_children( return min_width, width, min_height, height - def __css__(self): + def __css__(self) -> str: css = [] # display if self.display == NONE: diff --git a/core/src/toga/types.py b/core/src/toga/types.py index 594a1ba4e0..3fd6da367a 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -1,7 +1,16 @@ from __future__ import annotations +import sys from typing import NamedTuple +if sys.version_info < (3, 10): + from typing_extensions import ( # noqa:F401 + TypeAlias as TypeAlias, + TypeVar as TypeVar, + ) +else: + from typing import TypeAlias as TypeAlias, TypeVar as TypeVar # noqa:F401 + class LatLng(NamedTuple): """A geographic coordinate.""" @@ -12,5 +21,5 @@ class LatLng(NamedTuple): #: Longitude lng: float - def __str__(self): + def __str__(self) -> str: return f"({self.lat:6f}, {self.lng:6f})" diff --git a/core/src/toga/validators.py b/core/src/toga/validators.py index a68cb6e21e..83ae650919 100644 --- a/core/src/toga/validators.py +++ b/core/src/toga/validators.py @@ -30,7 +30,6 @@ def is_valid(self, input_string: str) -> bool: :param input_string: The string to validate. :returns: ``True`` if the input is valid. """ - ... class CountValidator: @@ -83,7 +82,6 @@ def count(self, input_string: str) -> int: :param input_string: The string to inspect for content of interest. :returns: The number of instances of content that the validator is looking for. """ - ... class LengthBetween(BooleanValidator): @@ -105,8 +103,8 @@ def __init__( ``True`` """ if error_message is None: - error_message = "Input should be between {} and {} characters".format( - min_length, max_length + error_message = ( + f"Input should be between {min_length} and {max_length} characters" ) super().__init__(error_message=error_message, allow_empty=allow_empty) @@ -146,9 +144,7 @@ def __init__( ``True`` """ if error_message is None: - error_message = "Input is too short (length should be at least {})".format( - length - ) + error_message = f"Input is too short (length should be at least {length})" super().__init__( min_length=length, max_length=None, @@ -170,9 +166,7 @@ def __init__( string is too long. """ if error_message is None: - error_message = "Input is too long (length should be at most {})".format( - length - ) + error_message = f"Input is too long (length should be at most {length})" super().__init__( min_length=None, max_length=length, @@ -294,7 +288,7 @@ def __init__( class MatchRegex(BooleanValidator): def __init__( self, - regex_string, + regex_string: str, error_message: str | None = None, allow_empty: bool = True, ): @@ -430,9 +424,7 @@ def __init__( else: expected_existence_message = "Input should contain at least one digit" expected_non_existence_message = "Input should not contain digits" - expected_count_message = "Input should contain exactly {} digits".format( - count - ) + expected_count_message = f"Input should contain exactly {count} digits" super().__init__( count=count, diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index 7681ce4fc6..f4d70d6d4b 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -2,14 +2,16 @@ from typing import Literal +from toga.style import Pack + from .base import Widget class ActivityIndicator(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, running: bool = False, ): """Create a new ActivityIndicator widget. @@ -27,7 +29,7 @@ def __init__( if running: self.start() - @property + @property # type: ignore[override] def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? @@ -41,7 +43,7 @@ def enabled(self, value: bool) -> None: pass def focus(self) -> None: - "No-op; ActivityIndicator cannot accept input focus" + """No-op; ActivityIndicator cannot accept input focus.""" pass @property diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index bb12d78d96..0cd99bd6f8 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 +from typing import TYPE_CHECKING, Any from travertino.node import Node @@ -17,11 +17,7 @@ class Widget(Node): _MIN_WIDTH = 100 _MIN_HEIGHT = 100 - def __init__( - self, - id: str | None = None, - style=None, - ): + def __init__(self, id: str | None = None, style: Pack | None = None): """Create a base Toga widget. This is an abstract base class; it cannot be instantiated. @@ -36,16 +32,16 @@ def __init__( ) self._id = str(id if id else identifier(self)) - self._window = None - self._app = None - self._impl = None + self._window: Window | None = None + self._app: App | None = None + self._impl: Any = None self.factory = get_platform_factory() def __repr__(self) -> str: return f"<{self.__class__.__name__}:0x{identifier(self):x}>" - def __lt__(self, other) -> bool: + def __lt__(self, other: Widget) -> bool: return self.id < other.id @property @@ -62,8 +58,7 @@ def tab_index(self) -> int | None: .. note:: - This is a beta feature. The ``tab_index`` API may change in - the future. + This is a beta feature. The ``tab_index`` API may change in the future. """ return self._impl.get_tab_index() @@ -71,7 +66,7 @@ def tab_index(self) -> int | None: def tab_index(self, tab_index: int) -> None: self._impl.set_tab_index(tab_index) - def _assert_can_have_children(self): + def _assert_can_have_children(self) -> None: if not self.can_have_children: raise ValueError(f"{type(self).__name__} cannot have children") diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index cc6a61b9ad..7733378721 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -1,5 +1,9 @@ from __future__ import annotations +from typing import Iterable + +from toga.style import Pack + from .base import Widget @@ -10,8 +14,8 @@ class Box(Widget): def __init__( self, id: str | None = None, - style=None, - children: list[Widget] | None = None, + style: Pack | None = None, + children: Iterable[Widget] | None = None, ): """Create a new Box container widget. @@ -26,7 +30,7 @@ def __init__( self._impl = self.factory.Box(interface=self) # Children need to be added *after* the impl has been created. - self._children = [] + self._children: list[Widget] = [] if children: self.add(*children) diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index d065425f42..1c4f9a7d36 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -1,9 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Protocol, Union import toga -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget @@ -11,14 +13,29 @@ from toga.icons import IconContent -class OnPressHandler(Protocol): - def __call__(self, widget: Button, **kwargs: Any) -> None: +class OnPressHandlerSync(Protocol): + def __call__(self, widget: Button, /) -> object: """A handler that will be invoked when a button is pressed. :param widget: The button that was pressed. - :param kwargs: Ensures compatibility with arguments added in future versions. """ - ... + + +class OnPressHandlerAsync(Protocol): + async def __call__(self, widget: Button, /) -> object: + """Async definition of :any:`OnPressHandlerSync`.""" + + +class OnPressHandlerGenerator(Protocol): + def __call__(self, widget: Button, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnPressHandlerSync`.""" + + +OnPressHandlerT: TypeAlias = Union[ + OnPressHandlerSync, + OnPressHandlerAsync, + OnPressHandlerGenerator, +] class Button(Widget): @@ -27,8 +44,8 @@ def __init__( text: str | None = None, icon: IconContent | None = None, id: str | None = None, - style=None, - on_press: OnPressHandler | None = None, + style: Pack | None = None, + on_press: OnPressHandlerT | None = None, enabled: bool = True, ): """Create a new button widget. @@ -50,18 +67,18 @@ def __init__( # 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 + self.on_press = None # type: ignore[assignment] # Set the content of the button - either an icon, or text, but not both. if icon: if text is not None: raise ValueError("Cannot specify both text and an icon") else: - self.icon = icon + self.icon = icon # type:ignore[assignment] else: - self.text = text + self.text = text # type:ignore[assignment] - self.on_press = on_press + self.on_press = on_press # type: ignore[assignment] self.enabled = enabled @property @@ -133,10 +150,10 @@ def icon(self, value: IconContent | None) -> None: self.refresh() @property - def on_press(self) -> OnPressHandler: + def on_press(self) -> WrappedHandlerT: """The handler to invoke when the button is pressed.""" return self._on_press @on_press.setter - def on_press(self, handler): + def on_press(self, handler: OnPressHandlerT) -> None: self._on_press = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index bebf336f90..9cad86144c 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -4,15 +4,28 @@ from abc import ABC, abstractmethod from contextlib import contextmanager from math import cos, pi, sin, tan -from typing import TYPE_CHECKING, Protocol +from typing import ( + TYPE_CHECKING, + Any, + ContextManager, + Iterator, + Literal, + NoReturn, + Protocol, +) from travertino.colors import Color import toga -from toga.colors import BLACK, color as parse_color +from toga.colors import BLACK, color as parse_color # type: ignore[attr-defined] from toga.constants import Baseline, FillRule -from toga.fonts import SYSTEM, SYSTEM_DEFAULT_FONT_SIZE, Font -from toga.handlers import wrapped_handler +from toga.fonts import ( # type: ignore[attr-defined] + SYSTEM, + SYSTEM_DEFAULT_FONT_SIZE, + Font, +) +from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.style import Pack from .base import Widget @@ -54,20 +67,20 @@ class DrawingObject(ABC): * :meth:`toga.widgets.canvas.WriteText ` """ - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}()" @abstractmethod - def _draw(self, impl, **kwargs): ... + def _draw(self, impl: Any, **kwargs: Any) -> None: ... class BeginPath(DrawingObject): - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.begin_path(**kwargs) class ClosePath(DrawingObject): - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.close_path(**kwargs) @@ -81,13 +94,13 @@ def __init__( self.color = color self.fill_rule = fill_rule - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(color={self.color!r}, " f"fill_rule={self.fill_rule})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.fill(self.color, self.fill_rule, **kwargs) @property @@ -95,7 +108,7 @@ def fill_rule(self) -> FillRule: return self._fill_rule @fill_rule.setter - def fill_rule(self, fill_rule: FillRule): + def fill_rule(self, fill_rule: FillRule) -> None: self._fill_rule = fill_rule @property @@ -103,7 +116,7 @@ def color(self) -> Color: return self._color @color.setter - def color(self, value: Color | str | None): + def color(self, value: Color | str | None) -> None: if value is None: self._color = parse_color(BLACK) else: @@ -115,10 +128,10 @@ def color(self, value: Color | str | None): # `context.fill()` used to be a context manager, but is now a primitive. # If you try to use the Fill drawing object as a context, raise an exception. - def __enter__(self): + def __enter__(self) -> NoReturn: raise RuntimeError("Context.fill() has been renamed Context.Fill().") - def __exit__(self): # pragma: no cover + def __exit__(self) -> None: # pragma: no cover # This method is required to make the object a context manager, but as the # __enter__ method raises an exception, the __exit__ can't be called. pass @@ -136,13 +149,13 @@ def __init__( self.line_width = line_width self.line_dash = line_dash - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(color={self.color!r}, " f"line_width={self.line_width}, line_dash={self.line_dash!r})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.stroke(self.color, self.line_width, self.line_dash, **kwargs) @property @@ -150,7 +163,7 @@ def color(self) -> Color: return self._color @color.setter - def color(self, value: Color | str | None): + def color(self, value: Color | str | None) -> None: if value is None: self._color = parse_color(BLACK) else: @@ -161,7 +174,7 @@ def line_width(self) -> float: return self._line_width @line_width.setter - def line_width(self, value: float): + def line_width(self, value: float) -> None: self._line_width = float(value) @property @@ -169,7 +182,7 @@ def line_dash(self) -> list[float] | None: return self._line_dash @line_dash.setter - def line_dash(self, value: list[float] | None): + def line_dash(self, value: list[float] | None) -> None: self._line_dash = value ########################################################################### @@ -178,10 +191,10 @@ def line_dash(self, value: list[float] | None): # `context.stroke()` used to be a context manager, but is now a primitive. # If you try to use the Stroke drawing object as a context, raise an exception. - def __enter__(self): + def __enter__(self) -> NoReturn: raise RuntimeError("Context.stroke() has been renamed Context.Stroke().") - def __exit__(self): # pragma: no cover + def __exit__(self) -> None: # pragma: no cover # This method is required to make the object a context manager, but as the # __enter__ method raises an exception, the __exit__ can't be called. pass @@ -192,10 +205,10 @@ def __init__(self, x: float, y: float): self.x = x self.y = y - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(x={self.x}, y={self.y})" - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.move_to(self.x, self.y, **kwargs) @@ -204,10 +217,10 @@ def __init__(self, x: float, y: float): self.x = x self.y = y - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(x={self.x}, y={self.y})" - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.line_to(self.x, self.y, **kwargs) @@ -222,14 +235,14 @@ def __init__( self.x = x self.y = y - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(cp1x={self.cp1x}, cp1y={self.cp1y}, " f"cp2x={self.cp2x}, cp2y={self.cp2y}, " f"x={self.x}, y={self.y})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.bezier_curve_to( self.cp1x, self.cp1y, self.cp2x, self.cp2y, self.x, self.y, **kwargs ) @@ -242,10 +255,10 @@ def __init__(self, cpx: float, cpy: float, x: float, y: float): self.x = x self.y = y - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(cpx={self.cpx}, cpy={self.cpy}, x={self.x}, y={self.y})" - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.quadratic_curve_to(self.cpx, self.cpy, self.x, self.y, **kwargs) @@ -266,14 +279,14 @@ def __init__( self.endangle = endangle self.anticlockwise = anticlockwise - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(x={self.x}, y={self.y}, " f"radius={self.radius}, startangle={self.startangle:.3f}, " f"endangle={self.endangle:.3f}, anticlockwise={self.anticlockwise})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.arc( self.x, self.y, @@ -306,7 +319,7 @@ def __init__( self.endangle = endangle self.anticlockwise = anticlockwise - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(x={self.x}, y={self.y}, " f"radiusx={self.radiusx}, radiusy={self.radiusy}, " @@ -314,7 +327,7 @@ def __repr__(self): f"endangle={self.endangle:.3f}, anticlockwise={self.anticlockwise})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.ellipse( self.x, self.y, @@ -335,13 +348,13 @@ def __init__(self, x: float, y: float, width: float, height: float): self.width = width self.height = height - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(x={self.x}, y={self.y}, " f"width={self.width}, height={self.height})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.rect(self.x, self.y, self.width, self.height, **kwargs) @@ -357,16 +370,16 @@ def __init__( self.text = text self.x = x self.y = y - self.font = font + self.font = font # type: ignore[assignment] self.baseline = baseline - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(text={self.text!r}, x={self.x}, y={self.y}, " f"font={self.font!r}, baseline={self.baseline})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.write_text( str(self.text), self.x, self.y, self.font._impl, self.baseline, **kwargs ) @@ -376,7 +389,7 @@ def font(self) -> Font: return self._font @font.setter - def font(self, value: Font | None): + def font(self, value: Font | None) -> None: if value is None: self._font = Font(family=SYSTEM, size=SYSTEM_DEFAULT_FONT_SIZE) else: @@ -387,10 +400,10 @@ class Rotate(DrawingObject): def __init__(self, radians: float): self.radians = radians - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(radians={self.radians:.3f})" - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.rotate(self.radians, **kwargs) @@ -399,10 +412,10 @@ def __init__(self, sx: float, sy: float): self.sx = sx self.sy = sy - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(sx={self.sx:.3f}, sy={self.sy:.3f})" - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.scale(self.sx, self.sy, **kwargs) @@ -411,15 +424,15 @@ def __init__(self, tx: float, ty: float): self.tx = tx self.ty = ty - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(tx={self.tx}, ty={self.ty})" - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.translate(self.tx, self.ty, **kwargs) class ResetTransform(DrawingObject): - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.reset_transform(**kwargs) @@ -436,13 +449,13 @@ class Context(DrawingObject): or use :any:`Canvas.context` to access the root context of the canvas. """ - def __init__(self, canvas: toga.Canvas, **kwargs): + def __init__(self, canvas: toga.Canvas, **kwargs: Any): # kwargs used to support multiple inheritance super().__init__(**kwargs) self._canvas = canvas - self.drawing_objects = [] + self.drawing_objects: list[DrawingObject] = [] - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.push_context(**kwargs) for obj in self.drawing_objects: obj._draw(impl, **kwargs) @@ -457,7 +470,7 @@ def canvas(self) -> Canvas: """The canvas that is associated with this drawing context.""" return self._canvas - def redraw(self): + def redraw(self) -> None: """Calls :any:`Canvas.redraw` on the parent Canvas.""" self.canvas.redraw() @@ -473,7 +486,7 @@ def __getitem__(self, index: int) -> DrawingObject: """Returns the drawing object at the given index.""" return self.drawing_objects[index] - def append(self, obj: DrawingObject): + def append(self, obj: DrawingObject) -> None: """Append a drawing object to the context. :param obj: The drawing object to add to the context. @@ -481,7 +494,7 @@ def append(self, obj: DrawingObject): self.drawing_objects.append(obj) self.redraw() - def insert(self, index: int, obj: DrawingObject): + def insert(self, index: int, obj: DrawingObject) -> None: """Insert a drawing object into the context at a specific index. :param index: The index at which the drawing object should be inserted. @@ -490,7 +503,7 @@ def insert(self, index: int, obj: DrawingObject): self.drawing_objects.insert(index, obj) self.redraw() - def remove(self, obj: DrawingObject): + def remove(self, obj: DrawingObject) -> None: """Remove a drawing object from the context. :param obj: The drawing object to remove. @@ -498,7 +511,7 @@ def remove(self, obj: DrawingObject): self.drawing_objects.remove(obj) self.redraw() - def clear(self): + def clear(self) -> None: """Remove all drawing objects from the context.""" self.drawing_objects.clear() self.redraw() @@ -507,7 +520,7 @@ def clear(self): # Path manipulation ########################################################################### - def begin_path(self): + def begin_path(self) -> BeginPath: """Start a new path in the canvas context. :returns: The ``BeginPath`` :any:`DrawingObject` for the operation. @@ -516,7 +529,7 @@ def begin_path(self): self.append(begin_path) return begin_path - def close_path(self): + def close_path(self) -> ClosePath: """Close the current path in the canvas context. This closes the current path as a simple drawing operation. It should be paired @@ -530,7 +543,7 @@ def close_path(self): self.append(close_path) return close_path - def move_to(self, x: float, y: float): + def move_to(self, x: float, y: float) -> MoveTo: """Moves the current point of the canvas context without drawing. :param x: The x coordinate of the new current point. @@ -541,7 +554,7 @@ def move_to(self, x: float, y: float): self.append(move_to) return move_to - def line_to(self, x: float, y: float): + def line_to(self, x: float, y: float) -> LineTo: """Draw a line segment ending at a point in the canvas context. :param x: The x coordinate for the end point of the line segment. @@ -560,7 +573,7 @@ def bezier_curve_to( cp2y: float, x: float, y: float, - ): + ) -> BezierCurveTo: """Draw a Bézier curve in the canvas context. A Bézier curve requires three points. The first two are control points; the @@ -580,7 +593,13 @@ def bezier_curve_to( self.append(bezier_curve_to) return bezier_curve_to - def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float): + def quadratic_curve_to( + self, + cpx: float, + cpy: float, + x: float, + y: float, + ) -> QuadraticCurveTo: """Draw a quadratic curve in the canvas context. A quadratic curve requires two points. The first point is a control point; the @@ -608,7 +627,7 @@ def arc( startangle: float = 0.0, endangle: float = 2 * pi, anticlockwise: bool = False, - ): + ) -> Arc: """Draw a circular arc in the canvas context. A full circle will be drawn by default; an arc can be drawn by specifying a @@ -638,7 +657,7 @@ def ellipse( startangle: float = 0.0, endangle: float = 2 * pi, anticlockwise: bool = False, - ): + ) -> Ellipse: """Draw an elliptical arc in the canvas context. A full ellipse will be drawn by default; an arc can be drawn by specifying a @@ -671,7 +690,7 @@ def ellipse( self.append(ellipse) return ellipse - def rect(self, x: float, y: float, width: float, height: float): + def rect(self, x: float, y: float, width: float, height: float) -> Rect: """Draw a rectangle in the canvas context. :param x: The horizontal coordinate of the left of the rectangle. @@ -688,8 +707,8 @@ def fill( self, color: str = BLACK, fill_rule: FillRule = FillRule.NONZERO, - preserve=None, # DEPRECATED - ): + preserve: None = None, # DEPRECATED + ) -> Fill: """Fill the current path. The fill can use either the `Non-Zero @@ -703,7 +722,7 @@ def fill( :returns: The ``Fill`` :any:`DrawingObject` for the operation. """ if preserve is not None: - warnings.warn( + warnings.warn( # type: ignore[unreachable] "The `preserve` argument on fill() has been deprecated.", DeprecationWarning, ) @@ -717,7 +736,7 @@ def stroke( color: str = BLACK, line_width: float = 2.0, line_dash: list[float] | None = None, - ): + ) -> Stroke: """Draw the current path as a stroke. :param color: The color for the stroke. @@ -741,7 +760,7 @@ def write_text( y: float = 0.0, font: Font | None = None, baseline: Baseline = Baseline.ALPHABETIC, - ): + ) -> WriteText: """Write text at a given position in the canvas context. Drawing text is effectively a series of path operations, so the text will have @@ -762,7 +781,7 @@ def write_text( ########################################################################### # Transformations ########################################################################### - def rotate(self, radians: float): + def rotate(self, radians: float) -> Rotate: """Add a rotation to the canvas context. :param radians: The angle to rotate clockwise in radians. @@ -772,7 +791,7 @@ def rotate(self, radians: float): self.append(rotate) return rotate - def scale(self, sx: float, sy: float): + def scale(self, sx: float, sy: float) -> Scale: """Add a scaling transformation to the canvas context. :param sx: Scale factor for the X dimension. A negative value flips the @@ -785,7 +804,7 @@ def scale(self, sx: float, sy: float): self.append(scale) return scale - def translate(self, tx: float, ty: float): + def translate(self, tx: float, ty: float) -> Translate: """Add a translation to the canvas context. :param tx: Translation for the X dimension. @@ -796,7 +815,7 @@ def translate(self, tx: float, ty: float): self.append(translate) return translate - def reset_transform(self): + def reset_transform(self) -> ResetTransform: """Reset all transformations in the canvas context. :returns: A ``ResetTransform`` :any:`DrawingObject`. @@ -810,7 +829,7 @@ def reset_transform(self): ########################################################################### @contextmanager - def Context(self): + def Context(self) -> Iterator[Context]: """Construct and yield a new sub-:class:`~toga.widgets.canvas.Context` within this context. @@ -822,7 +841,11 @@ def Context(self): self.redraw() @contextmanager - def ClosedPath(self, x: float | None = None, y: float | None = None): + def ClosedPath( + self, + x: float | None = None, + y: float | None = None, + ) -> Iterator[ClosedPathContext]: """Construct and yield a new :class:`~toga.widgets.canvas.ClosedPath` sub-context that will draw a closed path, starting from an origin. @@ -846,7 +869,7 @@ def Fill( y: float | None = None, color: str = BLACK, fill_rule: FillRule = FillRule.NONZERO, - ): + ) -> Iterator[FillContext]: """Construct and yield a new :class:`~toga.widgets.canvas.Fill` sub-context within this context. @@ -886,7 +909,7 @@ def Stroke( color: str = BLACK, line_width: float = 2.0, line_dash: list[float] | None = None, - ): + ) -> Iterator[StrokeContext]: """Construct and yield a new :class:`~toga.widgets.canvas.Stroke` sub-context within this context. @@ -924,7 +947,7 @@ def Stroke( # 2023-07 Backwards incompatibility ########################################################################### - def new_path(self): + def new_path(self) -> BeginPath: """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.begin_path`.""" warnings.warn( "Context.new_path() has been renamed Context.begin_path()", @@ -932,14 +955,14 @@ def new_path(self): ) return self.begin_path() - def context(self): + def context(self): # type: ignore """**DEPRECATED** - use :meth:`~toga.widgets.canvas.Context.Context`""" warnings.warn( "Context.context() has been renamed Context.Context()", DeprecationWarning ) return self.Context() - def closed_path(self, x: float, y: float): + def closed_path(self, x: float, y: float): # type: ignore """**DEPRECATED** - use :meth:`~toga.widgets.canvas.Context.ClosedPath`""" warnings.warn( "Context.closed_path() has been renamed Context.ClosedPath()", @@ -976,10 +999,10 @@ def __init__( self.x = x self.y = y - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(x={self.x}, y={self.y})" - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: """Used by parent to draw all objects that are part of the context.""" impl.push_context(**kwargs) impl.begin_path(**kwargs) @@ -1029,13 +1052,13 @@ def __init__( self.color = color self.fill_rule = fill_rule - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(x={self.x}, y={self.y}, " f"color={self.color!r}, fill_rule={self.fill_rule})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.push_context(**kwargs) impl.begin_path(**kwargs) if self.x is not None and self.y is not None: @@ -1062,7 +1085,7 @@ def color(self) -> Color: return self._color @color.setter - def color(self, value: Color | str | None): + def color(self, value: Color | str | None) -> None: if value is None: self._color = parse_color(BLACK) else: @@ -1102,13 +1125,13 @@ def __init__( self.line_width = line_width self.line_dash = line_dash - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(x={self.x}, y={self.y}, color={self.color!r}, " f"line_width={self.line_width}, line_dash={self.line_dash!r})" ) - def _draw(self, impl, **kwargs): + def _draw(self, impl: Any, **kwargs: Any) -> None: impl.push_context(**kwargs) impl.begin_path(**kwargs) @@ -1139,7 +1162,7 @@ def color(self) -> Color: return self._color @color.setter - def color(self, value): + def color(self, value: object) -> None: if value is None: self._color = parse_color(BLACK) else: @@ -1152,7 +1175,7 @@ def color(self, value): class OnTouchHandler(Protocol): - def __call__(self, widget: Canvas, x: int, y: int, **kwargs): + def __call__(self, widget: Canvas, x: int, y: int, **kwargs: Any) -> None: """A handler that will be invoked when a :any:`Canvas` is touched with a finger or mouse. @@ -1161,11 +1184,10 @@ def __call__(self, widget: Canvas, x: int, y: int, **kwargs): :param y: Y coordinate, relative to the top edge of the canvas. :param kwargs: Ensures compatibility with arguments added in future versions. """ - ... class OnResizeHandler(Protocol): - def __call__(self, widget: Canvas, width: int, height: int, **kwargs): + def __call__(self, widget: Canvas, width: int, height: int, **kwargs: Any) -> None: """A handler that will be invoked when a :any:`Canvas` is resized. :param widget: The canvas that was resized. @@ -1173,7 +1195,6 @@ def __call__(self, widget: Canvas, width: int, height: int, **kwargs): :param height: The new height. :param kwargs: Ensures compatibility with arguments added in future versions. """ - ... ####################################################################################### @@ -1187,16 +1208,16 @@ class Canvas(Widget): def __init__( self, - id=None, - style=None, - on_resize: OnResizeHandler = None, - on_press: OnTouchHandler = None, - on_activate: OnTouchHandler = None, - on_release: OnTouchHandler = None, - on_drag: OnTouchHandler = None, - on_alt_press: OnTouchHandler = None, - on_alt_release: OnTouchHandler = None, - on_alt_drag: OnTouchHandler = None, + id: str | None = None, + style: Pack | None = None, + on_resize: OnResizeHandler | None = None, + on_press: OnTouchHandler | None = None, + on_activate: OnTouchHandler | None = None, + on_release: OnTouchHandler | None = None, + on_drag: OnTouchHandler | None = None, + on_alt_press: OnTouchHandler | None = None, + on_alt_release: OnTouchHandler | None = None, + on_alt_drag: OnTouchHandler | None = None, ): """Create a new Canvas widget. @@ -1223,17 +1244,17 @@ def __init__( self._impl = self.factory.Canvas(interface=self) # Set all the properties - self.on_resize = on_resize - self.on_press = on_press - self.on_activate = on_activate - self.on_release = on_release - self.on_drag = on_drag - self.on_alt_press = on_alt_press - self.on_alt_release = on_alt_release - self.on_alt_drag = on_alt_drag - - @property - def enabled(self) -> bool: + self.on_resize = on_resize # type: ignore[assignment] + self.on_press = on_press # type: ignore[assignment] + self.on_activate = on_activate # type: ignore[assignment] + self.on_release = on_release # type: ignore[assignment] + self.on_drag = on_drag # type: ignore[assignment] + self.on_alt_press = on_alt_press # type: ignore[assignment] + self.on_alt_release = on_alt_release # type: ignore[assignment] + self.on_alt_drag = on_alt_drag # type: ignore[assignment] + + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? ScrollContainer widgets cannot be disabled; this property will always return True; any attempt to modify it will be ignored. @@ -1241,11 +1262,11 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; ScrollContainer cannot accept input focus" + def focus(self) -> None: + """No-op; ScrollContainer cannot accept input focus.""" pass @property @@ -1253,7 +1274,7 @@ def context(self) -> Context: """The root context for the canvas.""" return self._context - def redraw(self): + def redraw(self) -> None: """Redraw the Canvas. The Canvas will be automatically redrawn after adding or removing a drawing @@ -1262,7 +1283,7 @@ def redraw(self): """ self._impl.redraw() - def Context(self): + def Context(self) -> ContextManager[Context]: """Construct and yield a new sub-:class:`~toga.widgets.canvas.Context` within the root context of this Canvas. @@ -1270,7 +1291,11 @@ def Context(self): """ return self.context.Context() - def ClosedPath(self, x: float | None = None, y: float | None = None): + def ClosedPath( + self, + x: float | None = None, + y: float | None = None, + ) -> ContextManager[ClosedPathContext]: """Construct and yield a new :class:`~toga.widgets.canvas.ClosedPathContext` context in the root context of this canvas. @@ -1286,7 +1311,7 @@ def Fill( y: float | None = None, color: Color | str | None = BLACK, fill_rule: FillRule = FillRule.NONZERO, - ): + ) -> ContextManager[FillContext]: """Construct and yield a new :class:`~toga.widgets.canvas.FillContext` in the root context of this canvas. @@ -1303,7 +1328,7 @@ def Fill( :param color: The fill color. :yields: The new :class:`~toga.widgets.canvas.FillContext` context object. """ - return self.context.Fill(x, y, color, fill_rule) + return self.context.Fill(x, y, color, fill_rule) # type: ignore[arg-type] def Stroke( self, @@ -1312,7 +1337,7 @@ def Stroke( color: Color | str | None = BLACK, line_width: float = 2.0, line_dash: list[float] | None = None, - ): + ) -> ContextManager[StrokeContext]: """Construct and yield a new :class:`~toga.widgets.canvas.StrokeContext` in the root context of this canvas. @@ -1327,29 +1352,29 @@ def Stroke( solid line. :yields: The new :class:`~toga.widgets.canvas.StrokeContext` context object. """ - return self.context.Stroke(x, y, color, line_width, line_dash) + return self.context.Stroke(x, y, color, line_width, line_dash) # type: ignore[arg-type] @property - def on_resize(self) -> OnResizeHandler: + def on_resize(self) -> WrappedHandlerT: """The handler to invoke when the canvas is resized.""" return self._on_resize @on_resize.setter - def on_resize(self, handler: OnResizeHandler): + def on_resize(self, handler: OnResizeHandler) -> None: self._on_resize = wrapped_handler(self, handler) @property - def on_press(self) -> OnTouchHandler: + def on_press(self) -> WrappedHandlerT: """The handler invoked when the canvas is pressed. When a mouse is being used, this press will be with the primary (usually the left) mouse button.""" return self._on_press @on_press.setter - def on_press(self, handler: OnTouchHandler): + def on_press(self, handler: OnTouchHandler) -> None: self._on_press = wrapped_handler(self, handler) @property - def on_activate(self) -> OnTouchHandler: + def on_activate(self) -> WrappedHandlerT: """The handler invoked when the canvas is pressed in a way indicating the pressed object should be activated. When a mouse is in use, this will usually be a double click with the primary (usually the left) mouse button. @@ -1358,29 +1383,29 @@ def on_activate(self) -> OnTouchHandler: return self._on_activate @on_activate.setter - def on_activate(self, handler: OnTouchHandler): + def on_activate(self, handler: OnTouchHandler) -> None: self._on_activate = wrapped_handler(self, handler) @property - def on_release(self) -> OnTouchHandler: + def on_release(self) -> WrappedHandlerT: """The handler invoked when a press on the canvas ends.""" return self._on_release @on_release.setter - def on_release(self, handler: OnTouchHandler): + def on_release(self, handler: OnTouchHandler) -> None: self._on_release = wrapped_handler(self, handler) @property - def on_drag(self) -> OnTouchHandler: + def on_drag(self) -> WrappedHandlerT: """The handler invoked when the location of a press changes.""" return self._on_drag @on_drag.setter - def on_drag(self, handler: OnTouchHandler): + def on_drag(self, handler: OnTouchHandler) -> None: self._on_drag = wrapped_handler(self, handler) @property - def on_alt_press(self) -> OnTouchHandler: + def on_alt_press(self) -> WrappedHandlerT: """The handler to invoke when the canvas is pressed in an alternate manner. This will usually correspond to a secondary (usually the right) mouse button press. @@ -1390,11 +1415,11 @@ def on_alt_press(self) -> OnTouchHandler: return self._on_alt_press @on_alt_press.setter - def on_alt_press(self, handler: OnTouchHandler): + def on_alt_press(self, handler: OnTouchHandler) -> None: self._on_alt_press = wrapped_handler(self, handler) @property - def on_alt_release(self) -> OnTouchHandler: + def on_alt_release(self) -> WrappedHandlerT: """The handler to invoke when an alternate press is released. This event is not supported on Android or iOS. @@ -1402,11 +1427,11 @@ def on_alt_release(self) -> OnTouchHandler: return self._on_alt_release @on_alt_release.setter - def on_alt_release(self, handler: OnTouchHandler): + def on_alt_release(self, handler: OnTouchHandler) -> None: self._on_alt_release = wrapped_handler(self, handler) @property - def on_alt_drag(self) -> OnTouchHandler: + def on_alt_drag(self) -> WrappedHandlerT: """The handler to invoke when the location of an alternate press changes. This event is not supported on Android or iOS. @@ -1414,7 +1439,7 @@ def on_alt_drag(self) -> OnTouchHandler: return self._on_alt_drag @on_alt_drag.setter - def on_alt_drag(self, handler: OnTouchHandler): + def on_alt_drag(self, handler: OnTouchHandler) -> None: self._on_alt_drag = wrapped_handler(self, handler) ########################################################################### @@ -1425,7 +1450,7 @@ def measure_text( self, text: str, font: Font | None = None, - tight=None, # DEPRECATED + tight: None = None, # DEPRECATED ) -> tuple[float, float]: """Measure the size at which :meth:`~.Context.write_text` would render some text. @@ -1436,7 +1461,7 @@ def measure_text( :returns: A tuple of ``(width, height)``. """ if tight is not None: - warnings.warn( + warnings.warn( # type: ignore[unreachable] "The `tight` argument on Canvas.measure_text() has been deprecated.", DeprecationWarning, ) @@ -1449,7 +1474,7 @@ def measure_text( # As image ########################################################################### - def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: + def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: # type: ignore[assignment] """Render the canvas as an image. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also @@ -1464,7 +1489,7 @@ def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: # 2023-07 Backwards compatibility ########################################################################### - def new_path(self): + def new_path(self): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.begin_path` on :attr:`context`""" warnings.warn( @@ -1473,7 +1498,7 @@ def new_path(self): ) return self.context.begin_path() - def move_to(self, x, y): + def move_to(self, x, y): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.move_to` on :attr:`context`""" warnings.warn( @@ -1482,7 +1507,7 @@ def move_to(self, x, y): ) return self.context.move_to(x, y) - def line_to(self, x, y): + def line_to(self, x, y): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.line_to` on :attr:`context`""" warnings.warn( @@ -1491,7 +1516,7 @@ def line_to(self, x, y): ) return self.context.line_to(x, y) - def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y): + def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.bezier_curve_to` on :attr:`context`""" warnings.warn( @@ -1500,7 +1525,7 @@ def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y): ) return self.context.bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y) - def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float): + def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.quadratic_curve_to` on :attr:`context`""" warnings.warn( @@ -1509,7 +1534,7 @@ def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float): ) return self.context.quadratic_curve_to(cpx, cpy, x, y) - def arc(self, x, y, radius, startangle=0.0, endangle=2 * pi, anticlockwise=False): + def arc(self, x, y, radius, startangle=0.0, endangle=2 * pi, anticlockwise=False): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.arc` on :attr:`context`""" warnings.warn( @@ -1518,7 +1543,7 @@ def arc(self, x, y, radius, startangle=0.0, endangle=2 * pi, anticlockwise=False ) return self.context.arc(x, y, radius, startangle, endangle, anticlockwise) - def ellipse( + def ellipse( # type: ignore self, x: float, y: float, @@ -1546,7 +1571,7 @@ def ellipse( anticlockwise, ) - def rect(self, x: float, y: float, width: float, height: float): + def rect(self, x: float, y: float, width: float, height: float): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.rect` on :attr:`context`""" warnings.warn( @@ -1555,7 +1580,7 @@ def rect(self, x: float, y: float, width: float, height: float): ) return self.context.rect(x, y, width, height) - def write_text(self, text: str, x=0, y=0, font=None): + def write_text(self, text: str, x=0, y=0, font=None): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.write_text` on :attr:`context`""" warnings.warn( @@ -1564,7 +1589,7 @@ def write_text(self, text: str, x=0, y=0, font=None): ) return self.context.write_text(text, x, y, font) - def rotate(self, radians: float): + def rotate(self, radians: float): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.rotate` on :attr:`context`""" warnings.warn( @@ -1573,7 +1598,7 @@ def rotate(self, radians: float): ) return self.context.rotate(radians) - def scale(self, sx: float, sy: float): + def scale(self, sx: float, sy: float): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.scale` on :attr:`context`""" warnings.warn( "Direct canvas operations have been deprecated; use context.scale()", @@ -1581,7 +1606,7 @@ def scale(self, sx: float, sy: float): ) return self.context.scale(sx, sy) - def translate(self, tx: float, ty: float): + def translate(self, tx: float, ty: float): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.translate` on :attr:`context`""" warnings.warn( @@ -1590,7 +1615,7 @@ def translate(self, tx: float, ty: float): ) return self.context.translate(tx, ty) - def reset_transform(self): + def reset_transform(self): # type: ignore """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.reset_transform` on :attr:`context`""" warnings.warn( @@ -1599,7 +1624,7 @@ def reset_transform(self): ) return self.context.reset_transform() - def closed_path(self, x, y): + def closed_path(self, x, y): # type: ignore """**DEPRECATED** - use :meth:`~toga.Canvas.ClosedPath`""" warnings.warn( "Canvas.closed_path() has been renamed Canvas.ClosedPath()", @@ -1607,7 +1632,7 @@ def closed_path(self, x, y): ) return self.ClosedPath(x, y) - def fill( + def fill( # type: ignore self, color: Color | str | None = BLACK, fill_rule: FillRule = FillRule.NONZERO, @@ -1625,7 +1650,7 @@ def fill( ) return self.Fill(color=color, fill_rule=fill_rule) - def stroke( + def stroke( # type: ignore self, color: Color | str | None = BLACK, line_width: float = 2.0, @@ -1639,7 +1664,7 @@ def stroke( return self.Stroke(color=color, line_width=line_width, line_dash=line_dash) -def sweepangle(startangle, endangle, anticlockwise): +def sweepangle(startangle: float, endangle: float, anticlockwise: bool) -> float: """Returns an arc length in the range [-2 * pi, 2 * pi], where positive numbers are clockwise. Based on the "ellipse method steps" in the HTML spec.""" @@ -1664,7 +1689,7 @@ def sweepangle(startangle, endangle, anticlockwise): # Based on https://stackoverflow.com/a/30279817 -def arc_to_bezier(sweepangle): +def arc_to_bezier(sweepangle: float) -> list[tuple[float, float]]: """Approximates an arc of a unit circle as a sequence of Bezier segments. :param sweepangle: Length of the arc in radians, where positive numbers are @@ -1707,7 +1732,7 @@ def arc_to_bezier(sweepangle): return result -def transform(x, y, matrix): +def transform(x: float, y: float, matrix: list[int]) -> tuple[float, float]: return ( x * matrix[0] + y * matrix[1], x * matrix[2] + y * matrix[3], diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 53b050816b..c742be9175 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -2,8 +2,11 @@ import datetime import warnings +from typing import Any, Protocol, Union -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget @@ -15,17 +18,39 @@ MAX_DATE = datetime.date(8999, 12, 31) +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler that will be invoked when the value changes.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, + OnChangeHandlerAsync, + OnChangeHandlerGenerator, +] + + class DateInput(Widget): _MIN_WIDTH = 200 def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, value: datetime.date | None = None, min: datetime.date | None = None, max: datetime.date | None = None, - on_change: callable | None = None, + on_change: OnChangeHandlerT | None = None, ): """Create a new DateInput widget. @@ -43,12 +68,12 @@ def __init__( # 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 + self.on_change = None # type: ignore[assignment] + self.min = min # type: ignore[assignment] + self.max = max # type: ignore[assignment] - self.value = value - self.on_change = on_change + self.value = value # type: ignore[assignment] + self.on_change = on_change # type: ignore[assignment] @property def value(self) -> datetime.date: @@ -60,7 +85,18 @@ def value(self) -> datetime.date: """ return self._impl.get_value() - def _convert_date(self, value, *, check_range): + @value.setter + def value(self, value: object) -> None: + value = self._convert_date(value, check_range=False) + + if value < self.min: + value = self.min + elif value > self.max: + value = self.max + + self._impl.set_value(value) + + def _convert_date(self, value: object, *, check_range: bool) -> datetime.date: if value is None: value = datetime.date.today() elif isinstance(value, datetime.datetime): @@ -82,17 +118,6 @@ def _convert_date(self, value, *, check_range): return value - @value.setter - def value(self, value): - value = self._convert_date(value, check_range=False) - - if value < self.min: - value = self.min - elif value > self.max: - value = self.max - - self._impl.set_value(value) - @property def min(self) -> datetime.date: """The minimum allowable date (inclusive). A value of ``None`` will be converted @@ -106,7 +131,7 @@ def min(self) -> datetime.date: return self._impl.get_min_date() @min.setter - def min(self, value): + def min(self, value: object) -> None: if value is None: min = MIN_DATE else: @@ -131,7 +156,7 @@ def max(self) -> datetime.date: return self._impl.get_max_date() @max.setter - def max(self, value): + def max(self, value: object) -> None: if value is None: max = MAX_DATE else: @@ -144,18 +169,18 @@ def max(self, value): self.value = max @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """The handler to invoke when the date value changes.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) # 2023-05: Backwards compatibility class DatePicker(DateInput): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): warnings.warn("DatePicker has been renamed DateInput.", DeprecationWarning) for old_name, new_name in [ @@ -176,29 +201,29 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @property - def min_date(self): + def min_date(self) -> datetime.date: warnings.warn( "DatePicker.min_date has been renamed DateInput.min", DeprecationWarning ) return self.min @min_date.setter - def min_date(self, value): + def min_date(self, value: object) -> None: warnings.warn( "DatePicker.min_date has been renamed DateInput.min", DeprecationWarning ) - self.min = value + self.min = value # type: ignore[assignment] @property - def max_date(self): + def max_date(self) -> datetime.date: warnings.warn( "DatePicker.max_date has been renamed DateInput.max", DeprecationWarning ) return self.max @max_date.setter - def max_date(self, value): + def max_date(self, value: object) -> None: warnings.warn( "DatePicker.max_date has been renamed DateInput.max", DeprecationWarning ) - self.max = value + self.max = value # type: ignore[assignment] diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 2b4d818f9a..5331f92ee8 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -1,29 +1,132 @@ from __future__ import annotations import warnings -from typing import Any - -from toga.handlers import wrapped_handler +from typing import ( + Any, + Generic, + Iterable, + Literal, + Protocol, + TypeVar, + Union, +) + +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +T = TypeVar("T") +SourceT = TypeVar("SourceT", bound=Source) + + +class OnPrimaryActionHandlerSync(Protocol): + def __call__(self, row: Any, /) -> object: + """A handler to invoke for the primary action. + + :param row: The current row for the detailed list. + """ + + +class OnPrimaryActionHandlerAsync(Protocol): + async def __call__(self, row: Any, /) -> object: + """Async definition of :any:`OnPrimaryActionHandlerSync`.""" + + +class OnPrimaryActionHandlerGenerator(Protocol): + def __call__(self, row: Any, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnPrimaryActionHandlerSync`.""" + + +OnPrimaryActionHandlerT: TypeAlias = Union[ + OnPrimaryActionHandlerSync, + OnPrimaryActionHandlerAsync, + OnPrimaryActionHandlerGenerator, +] + + +class OnSecondaryActionHandlerSync(Protocol): + def __call__(self, row: Any, /) -> object: + """A handler to invoke for the secondary action. + + :param row: The current row for the detailed list. + """ + + +class OnSecondaryActionHandlerAsync(Protocol): + async def __call__(self, row: Any, /) -> object: + """Async definition of :any:`OnSecondaryActionHandlerSync`.""" + + +class OnSecondaryActionHandlerGenerator(Protocol): + def __call__(self, row: Any, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnSecondaryActionHandlerSync`.""" + + +OnSecondaryActionHandlerT: TypeAlias = Union[ + OnSecondaryActionHandlerSync, + OnSecondaryActionHandlerAsync, + OnSecondaryActionHandlerGenerator, +] -class DetailedList(Widget): + +class OnRefreshHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the detailed list is refreshed.""" + + +class OnRefreshHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnRefreshHandlerSync`.""" + + +class OnRefreshHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnRefreshHandlerSync`.""" + + +OnRefreshHandlerT: TypeAlias = Union[ + OnRefreshHandlerSync, OnRefreshHandlerAsync, OnRefreshHandlerGenerator +] + + +class OnSelectHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the detailed list is selected.""" + + +class OnSelectHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnSelectHandlerSync`.""" + + +class OnSelectHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnSelectHandlerSync`.""" + + +OnSelectHandlerT: TypeAlias = Union[ + OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator +] + + +class DetailedList(Widget, Generic[T]): def __init__( self, - id=None, - style=None, - data: Any = None, + id: str | None = None, + style: Pack | None = None, + data: SourceT | Iterable[T] | None = None, accessors: tuple[str, str, str] = ("title", "subtitle", "icon"), missing_value: str = "", primary_action: str | None = "Delete", - on_primary_action: callable = None, + on_primary_action: OnPrimaryActionHandlerT | None = None, secondary_action: str | None = "Action", - on_secondary_action: callable = None, - on_refresh: callable = None, - on_select: callable = None, - on_delete: callable = None, # DEPRECATED + on_secondary_action: OnSecondaryActionHandlerT | None = None, + on_refresh: OnRefreshHandlerT | None = None, + on_select: OnSelectHandlerT | None = None, + on_delete: None = None, # DEPRECATED ): """Create a new DetailedList widget. @@ -49,7 +152,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if on_delete: - if on_primary_action: + if on_primary_action: # type: ignore[unreachable] raise ValueError("Cannot specify both on_delete and on_primary_action") else: warnings.warn( @@ -63,22 +166,24 @@ def __init__( # 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._missing_value = missing_value - self._data = None - self.on_select = None + self.on_select = None # type: ignore[assignment] + + # TODO:PR: in reality, _data needs to be Sized and SupportsIndex... + self._data: SourceT | ListSource[T] = None # type: ignore[assignment] self._impl = self.factory.DetailedList(interface=self) - self.data = data - self.on_primary_action = on_primary_action - self.on_secondary_action = on_secondary_action - self.on_refresh = on_refresh - self.on_select = on_select + self.data = data # type: ignore[assignment] + self.on_primary_action = on_primary_action # type: ignore[assignment] + self.on_secondary_action = on_secondary_action # type: ignore[assignment] + self.on_refresh = on_refresh # type: ignore[assignment] + self.on_select = on_select # type: ignore[assignment] - @property - def enabled(self) -> bool: + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? DetailedList widgets cannot be disabled; this property will always return True; any attempt to modify it will be ignored. @@ -86,15 +191,15 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; DetailedList cannot accept input focus" + def focus(self) -> None: + """No-op; DetailedList cannot accept input focus.""" pass @property - def data(self) -> ListSource: + def data(self) -> SourceT | ListSource[T]: """The data to display in the table. When setting this property: @@ -110,39 +215,39 @@ def data(self) -> ListSource: return self._data @data.setter - def data(self, data: Any): + def data(self, data: SourceT | Iterable[T] | None) -> None: if data is None: self._data = ListSource(data=[], accessors=self.accessors) elif isinstance(data, Source): - self._data = data + self._data = data # type: ignore[assignment] else: self._data = ListSource(data=data, accessors=self.accessors) self._data.add_listener(self._impl) self._impl.change_source(source=self._data) - def scroll_to_top(self): + def scroll_to_top(self) -> None: """Scroll the view so that the top of the list (first row) is visible.""" self.scroll_to_row(0) - def scroll_to_row(self, row: int): + def scroll_to_row(self, row: int) -> None: """Scroll the view so that the specified row index is visible. :param row: The index of the row to make visible. Negative values refer to the nth last row (-1 is the last row, -2 second last, and so on). """ - if len(self.data) > 1: + if len(self.data) > 1: # type: ignore[arg-type] if row >= 0: - self._impl.scroll_to_row(min(row, len(self.data))) + self._impl.scroll_to_row(min(row, len(self.data))) # type: ignore[arg-type] else: - self._impl.scroll_to_row(max(len(self.data) + row, 0)) + self._impl.scroll_to_row(max(len(self.data) + row, 0)) # type: ignore[arg-type] - def scroll_to_bottom(self): + def scroll_to_bottom(self) -> None: """Scroll the view so that the bottom of the list (last row) is visible.""" self.scroll_to_row(-1) @property - def accessors(self) -> list[str]: + def accessors(self) -> tuple[str, str, str]: """The accessors used to populate the list (read-only)""" return self._accessors @@ -154,18 +259,18 @@ def missing_value(self) -> str: return self._missing_value @property - def selection(self) -> Row | None: + def selection(self) -> Row[T] | None: """The current selection of the table. Returns the selected Row object, or :any:`None` if no row is currently selected. """ try: - return self.data[self._impl.get_selection()] + return self.data[self._impl.get_selection()] # type: ignore[index] except TypeError: return None @property - def on_primary_action(self) -> callable: + def on_primary_action(self) -> WrappedHandlerT: """The handler to invoke when the user performs the primary action on a row of the DetailedList. @@ -178,12 +283,12 @@ def on_primary_action(self) -> callable: return self._on_primary_action @on_primary_action.setter - def on_primary_action(self, handler: callable): + def on_primary_action(self, handler: OnPrimaryActionHandlerT) -> None: self._on_primary_action = wrapped_handler(self, handler) self._impl.set_primary_action_enabled(handler is not None) @property - def on_secondary_action(self) -> callable: + def on_secondary_action(self) -> WrappedHandlerT: """The handler to invoke when the user performs the secondary action on a row of the DetailedList. @@ -196,12 +301,12 @@ def on_secondary_action(self) -> callable: return self._on_secondary_action @on_secondary_action.setter - def on_secondary_action(self, handler: callable): + def on_secondary_action(self, handler: OnSecondaryActionHandlerT) -> None: self._on_secondary_action = wrapped_handler(self, handler) self._impl.set_secondary_action_enabled(handler is not None) @property - def on_refresh(self) -> callable: + def on_refresh(self) -> WrappedHandlerT: """The callback function to invoke when the user performs a refresh action (usually "pull down") on the DetailedList. @@ -211,19 +316,19 @@ def on_refresh(self) -> callable: return self._on_refresh @on_refresh.setter - def on_refresh(self, handler: callable): + def on_refresh(self, handler: OnRefreshHandlerT) -> None: self._on_refresh = wrapped_handler( self, handler, cleanup=self._impl.after_on_refresh ) self._impl.set_refresh_enabled(handler is not None) @property - def on_select(self) -> callable: + def on_select(self) -> WrappedHandlerT: """The callback function that is invoked when a row of the DetailedList is selected.""" return self._on_select @on_select.setter - def on_select(self, handler: callable): + def on_select(self, handler: OnSelectHandlerT) -> None: self._on_select = wrapped_handler(self, handler) ###################################################################### @@ -231,7 +336,7 @@ def on_select(self, handler: callable): ###################################################################### @property - def on_delete(self): + def on_delete(self) -> WrappedHandlerT: """**DEPRECATED**; Use :any:`on_primary_action`""" warnings.warn( "DetailedList.on_delete has been renamed DetailedList.on_primary_action.", @@ -240,7 +345,7 @@ def on_delete(self): return self.on_primary_action @on_delete.setter - def on_delete(self, handler): + def on_delete(self, handler: OnPrimaryActionHandlerT) -> None: warnings.warn( "DetailedList.on_delete has been renamed DetailedList.on_primary_action.", DeprecationWarning, diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index c9103974c1..2469642bce 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import Literal + from toga.constants import Direction +from toga.style import Pack from .base import Widget @@ -11,8 +14,8 @@ class Divider(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, direction: Direction = HORIZONTAL, ): """Create a new divider line. @@ -31,8 +34,8 @@ def __init__( self._impl = self.factory.Divider(interface=self) self.direction = direction - @property - def enabled(self) -> bool: + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? Divider widgets cannot be disabled; this property will always return True; any @@ -41,11 +44,11 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; Divider cannot accept input focus" + def focus(self) -> None: + """No-op; Divider cannot accept input focus.""" pass @property @@ -54,6 +57,6 @@ def direction(self) -> Direction: return self._impl.get_direction() @direction.setter - def direction(self, value): + def direction(self, value: object) -> None: self._impl.set_direction(value) self.refresh() diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index e269a2ce00..2f0c43c0b1 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -1,18 +1,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from travertino.size import at_least import toga -from toga.style.pack import NONE +from toga.style.pack import NONE, Pack from toga.widgets.base import Widget if TYPE_CHECKING: from toga.images import ImageContent, ImageT -def rehint_imageview(image, style, scale=1): +def rehint_imageview( + image: toga.Image, + style: Pack, + scale: int = 1, +) -> tuple[int, int, float | None]: """Compute the size hints for an ImageView based on the image. This logic is common across all backends, so it's shared here. @@ -69,8 +73,8 @@ class ImageView(Widget): def __init__( self, image: ImageContent | None = None, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, ): """ Create a new image view. @@ -85,10 +89,10 @@ def __init__( # Prime the image attribute self._image = None self._impl = self.factory.ImageView(interface=self) - self.image = image + self.image = image # type: ignore[assignment] - @property - def enabled(self) -> bool: + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? ImageView widgets cannot be disabled; this property will always return True; any @@ -97,11 +101,11 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; ImageView cannot accept input focus" + def focus(self) -> None: + """No-op; ImageView cannot accept input focus.""" pass @property @@ -114,7 +118,7 @@ def image(self) -> toga.Image | None: return self._image @image.setter - def image(self, image): + def image(self, image: ImageContent) -> None: if isinstance(image, toga.Image): self._image = image elif image is None: @@ -125,7 +129,7 @@ def image(self, image): self._impl.set_image(self._image) self.refresh() - def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: + def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: # type: ignore[assignment] """Return the image in the specified format. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also @@ -134,4 +138,5 @@ def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: `. :returns: The image in the specified format. """ - return self.image.as_format(format) + # TODO:PR: what's the use-case for initializing with image=None? cause this won't work then... + return self.image.as_format(format) # type: ignore[union-attr] diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 88a5fd1fad..4b822bd69f 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -1,5 +1,7 @@ from __future__ import annotations +from toga.style import Pack + from .base import Widget @@ -7,8 +9,8 @@ class Label(Widget): def __init__( self, text: str, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, ): """Create a new text label. @@ -24,8 +26,8 @@ def __init__( self.text = text - def focus(self): - "No-op; Label cannot accept input focus" + def focus(self) -> None: + """No-op; Label cannot accept input focus.""" pass @property @@ -39,7 +41,7 @@ def text(self) -> str: return self._impl.get_text() @text.setter - def text(self, value): + def text(self, value: object) -> None: if value is None or value == "\u200B": text = "" else: diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 5ebda6afca..97755db5bc 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -1,9 +1,11 @@ from __future__ import annotations -from typing import Any, Protocol +from typing import Any, Iterator, Protocol, Union import toga -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget @@ -27,10 +29,10 @@ def __init__( self._subtitle = subtitle # A pin isn't tied to a map at time of creation. - self.interface = None + self.interface: MapView = None # type:ignore[assignment] self._native = None - def __repr__(self): + def __repr__(self) -> str: if self.subtitle: label = f"; {self.title} - {self.subtitle}" else: @@ -40,7 +42,7 @@ def __repr__(self): @property def location(self) -> toga.LatLng: - "The (latitude, longitude) where the pin is located." + """The (latitude, longitude) where the pin is located.""" return self._location @location.setter @@ -75,26 +77,26 @@ def subtitle(self, subtitle: str | None) -> None: class MapPinSet: - def __init__(self, interface, pins): + def __init__(self, interface: MapView, pins: Iterator[MapPin] | None): self.interface = interface - self._pins = set() + self._pins: set[MapPin] = set() if pins: for item in pins: self.add(item) - def __repr__(self): + def __repr__(self) -> str: return f"" - def __iter__(self): + def __iter__(self) -> Iterator[MapPin]: """Return an iterator over the pins on the map.""" return iter(self._pins) - def __len__(self): + def __len__(self) -> int: """Return the number of pins being displayed.""" return len(self._pins) - def add(self, pin): + def add(self, pin: MapPin) -> None: """Add a new pin to the map. :param pin: The :any:`toga.MapPin` instance to add. @@ -103,42 +105,57 @@ def add(self, pin): self._pins.add(pin) self.interface._impl.add_pin(pin) - def remove(self, pin): + def remove(self, pin: MapPin) -> None: """Remove a pin from the map. :param pin: The :any:`toga.MapPin` instance to remove. """ self.interface._impl.remove_pin(pin) self._pins.remove(pin) - pin.interface = None + pin.interface = None # type:ignore[assignment] - def clear(self): + def clear(self) -> None: """Remove all pins from the map.""" for pin in self._pins: self.interface._impl.remove_pin(pin) self._pins = set() -class OnSelectHandler(Protocol): - def __call__(self, widget: MapView, *, pin: MapPin, **kwargs: Any) -> None: +class OnSelectHandlerSync(Protocol): + def __call__(self, widget: MapView, /, *, pin: MapPin) -> object: """A handler that will be invoked when the user selects a map pin. :param widget: The button that was pressed. :param pin: The pin that was selected. - :param kwargs: Ensures compatibility with arguments added in future versions. """ - ... + + +class OnSelectHandlerAsync(Protocol): + async def __call__(self, widget: MapView, /, *, pin: MapPin) -> object: + """Async definition of :any:`OnSelectHandlerSync`.""" + + +class OnSelectHandlerGenerator(Protocol): + def __call__( + self, widget: MapView, /, *, pin: MapPin + ) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnSelectHandlerSync`.""" + + +OnSelectHandlerT: TypeAlias = Union[ + OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator +] class MapView(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, location: toga.LatLng | tuple[float, float] | None = None, zoom: int = 11, - pins: list[MapPin] | None = None, - on_select: toga.widgets.mapview.OnSelectHandler | None = None, + pins: Iterator[MapPin] | None = None, + on_select: OnSelectHandlerT | None = None, ): """Create a new MapView widget. @@ -155,19 +172,19 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.MapView(interface=self) + self._impl: Any = self.factory.MapView(interface=self) self._pins = MapPinSet(self, pins) if location: - self.location = location + self.location = location # type:ignore[assignment] else: # Default location is Perth, Australia. Because why not? - self.location = (-31.9559, 115.8606) + self.location = (-31.9559, 115.8606) # type:ignore[assignment] self.zoom = zoom - self.on_select = on_select + self.on_select = on_select # type:ignore[assignment] @property def location(self) -> toga.LatLng: @@ -179,7 +196,7 @@ def location(self) -> toga.LatLng: return self._impl.get_location() @location.setter - def location(self, coordinates: toga.LatLng | tuple[float, float]): + def location(self, coordinates: toga.LatLng | tuple[float, float]) -> None: self._impl.set_location(toga.LatLng(*coordinates)) @property @@ -218,7 +235,7 @@ def zoom(self) -> int: return round(self._impl.get_zoom()) @zoom.setter - def zoom(self, value: int): + def zoom(self, value: int) -> None: value = int(value) if value < 0: value = 0 @@ -233,7 +250,7 @@ def pins(self) -> MapPinSet: return self._pins @property - def on_select(self) -> toga.widgets.mapview.OnSelectHandler: + def on_select(self) -> WrappedHandlerT: """The handler to invoke when the user selects a pin on a map. **Note:** This is not currently supported on GTK or Windows. @@ -241,7 +258,7 @@ def on_select(self) -> toga.widgets.mapview.OnSelectHandler: return self._on_select @on_select.setter - def on_select(self, handler: toga.widgets.mapview.OnSelectHandler | None): + def on_select(self, handler: OnSelectHandlerT | None) -> None: if handler and not getattr(self._impl, "SUPPORTS_ON_SELECT", True): self.factory.not_implemented("MapView.on_select") diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index d4db065abf..564f7ec69e 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -1,19 +1,43 @@ from __future__ import annotations -from toga.handlers import wrapped_handler +from typing import Protocol, Union + +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the value is changed.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator +] + + class MultilineTextInput(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, value: str | None = None, readonly: bool = False, placeholder: str | None = None, - on_change: callable | None = None, + on_change: OnChangeHandlerT | None = None, ): """Create a new multi-line text input widget. @@ -35,13 +59,13 @@ def __init__( # Set a dummy handler before installing the actual on_change, because we do not want # on_change triggered by the initial value being set - self.on_change = None - self.value = value + self.on_change = None # type: ignore[assignment] + self.value = value # type: ignore[assignment] # Set all the properties self.readonly = readonly - self.placeholder = placeholder - self.on_change = on_change + self.placeholder = placeholder # type: ignore[assignment] + self.on_change = on_change # type: ignore[assignment] @property def placeholder(self) -> str: @@ -53,7 +77,7 @@ def placeholder(self) -> str: return self._impl.get_placeholder() @placeholder.setter - def placeholder(self, value): + def placeholder(self, value: object) -> None: self._impl.set_placeholder("" if value is None else str(value)) self.refresh() @@ -68,7 +92,7 @@ def readonly(self) -> bool: return self._impl.get_readonly() @readonly.setter - def readonly(self, value): + def readonly(self, value: object) -> None: self._impl.set_readonly(bool(value)) @property @@ -81,23 +105,23 @@ def value(self) -> str: return self._impl.get_value() @value.setter - def value(self, value): + def value(self, value: object) -> None: self._impl.set_value("" if value is None else str(value)) self.refresh() - def scroll_to_bottom(self): + def scroll_to_bottom(self) -> None: """Scroll the view to make the bottom of the text field visible.""" self._impl.scroll_to_bottom() - def scroll_to_top(self): + def scroll_to_top(self) -> None: """Scroll the view to make the top of the text field visible.""" self._impl.scroll_to_top() @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """The handler to invoke when the value of the widget changes.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 83690fd36b..e9636746f8 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -3,8 +3,11 @@ import re import warnings from decimal import ROUND_HALF_UP, Decimal, InvalidOperation +from typing import Protocol, Union -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget @@ -22,8 +25,11 @@ NUMERIC_RE = re.compile(r"[^0-9\.-]") +NumberInputT: TypeAlias = Union[Decimal, float, str] +StepInputT: TypeAlias = Union[Decimal, int] -def _clean_decimal(value, step=None): + +def _clean_decimal(value: NumberInputT, step: StepInputT | None = None) -> Decimal: # Decimal(3.7) yields "3.700000000...177". # However, Decimal(str(3.7)) yields "3.7". If the user provides a float, # convert to a string first. @@ -40,7 +46,7 @@ def _clean_decimal(value, step=None): return value -def _clean_decimal_str(value): +def _clean_decimal_str(value: str) -> str: """Clean a string value""" # Replace any character that isn't a number, `.` or `-` value = NUMERIC_RE.sub("", value) @@ -61,19 +67,39 @@ def _clean_decimal_str(value): return value +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the value is changed.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator +] + + class NumberInput(Widget): def __init__( self, - id=None, - style=None, - step: Decimal = 1, - min: Decimal | None = None, - max: Decimal | None = None, - value: Decimal | None = None, + id: str | None = None, + style: Pack | None = None, + step: StepInputT = 1, + min: NumberInputT | None = None, + max: NumberInputT | None = None, + value: NumberInputT | None = None, readonly: bool = False, - on_change: callable | None = None, - min_value: Decimal | None = None, # DEPRECATED - max_value: Decimal | None = None, # DEPRECATED + on_change: OnChangeHandlerT | None = None, + min_value: None = None, # DEPRECATED + max_value: None = None, # DEPRECATED ): """Create a new number input widget. @@ -88,8 +114,8 @@ def __init__( equal to this maximum. :param value: The initial value for the widget. :param readonly: Can the value of the widget be modified by the user? - :param on_change: A handler that will be invoked when the value of the - widget changes. + :param on_change: A handler that will be invoked when the value of the widget + changes. :param min_value: **DEPRECATED**; alias of ``min``. :param max_value: **DEPRECATED**; alias of ``max``. """ @@ -99,7 +125,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if min_value is not None: - if min is not None: + if min is not None: # type: ignore[unreachable] raise ValueError("Cannot specify both min and min_value") else: warnings.warn( @@ -108,7 +134,7 @@ def __init__( ) min = min_value if max_value is not None: - if max is not None: + if max is not None: # type: ignore[unreachable] raise ValueError("Cannot specify both max and max_value") else: warnings.warn( @@ -123,19 +149,19 @@ def __init__( # The initial setting of min requires calling get_value(), # which in turn interrogates min. Prime those values with # an empty starting value - self._min = None - self._max = None + self._min: Decimal | None = None + self._max: Decimal | None = None - self.on_change = None + self.on_change = None # type: ignore[assignment] self._impl = self.factory.NumberInput(interface=self) self.readonly = readonly - self.step = step - self.min = min - self.max = max - self.value = value + self.step = step # type: ignore[assignment] + self.min = min # type: ignore[assignment] + self.max = max # type: ignore[assignment] + self.value = value # type: ignore[assignment] - self.on_change = on_change + self.on_change = on_change # type: ignore[assignment] @property def readonly(self) -> bool: @@ -148,7 +174,7 @@ def readonly(self) -> bool: return self._impl.get_readonly() @readonly.setter - def readonly(self, value): + def readonly(self, value: object) -> None: self._impl.set_readonly(value) @property @@ -160,7 +186,7 @@ def step(self) -> Decimal: return self._step @step.setter - def step(self, step): + def step(self, step: StepInputT) -> None: try: self._step = _clean_decimal(step) except (ValueError, TypeError, InvalidOperation): @@ -168,7 +194,7 @@ def step(self, step): self._impl.set_step(self._step) - # Re-assigning the min and max value forces the min/max to be requantized. + # Re-assigning the min and max value forces the min/max to be re-quantized. self.min = self.min self.max = self.max @@ -184,9 +210,9 @@ def min(self) -> Decimal | None: return self._min @min.setter - def min(self, new_min): + def min(self, new_min: NumberInputT | None) -> None: try: - new_min = _clean_decimal(new_min, self.step) + new_min = _clean_decimal(new_min, self.step) # type: ignore[arg-type] # Clip widget's value to the new minimum if self.value is not None and self.value < new_min: @@ -216,9 +242,9 @@ def max(self) -> Decimal | None: return self._max @max.setter - def max(self, new_max): + def max(self, new_max: NumberInputT | None) -> None: try: - new_max = _clean_decimal(new_max, self.step) + new_max = _clean_decimal(new_max, self.step) # type: ignore[arg-type] # Clip widget's value to the new maximum if self.value is not None and self.value > new_max: @@ -263,9 +289,9 @@ def value(self) -> Decimal | None: return value @value.setter - def value(self, value): + def value(self, value: NumberInputT | None) -> None: try: - value = _clean_decimal(value, self.step) + value = _clean_decimal(value, self.step) # type: ignore[arg-type] if self.min is not None and value < self.min: value = self.min @@ -281,12 +307,12 @@ def value(self, value): self.refresh() @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """The handler to invoke when the value of the widget changes.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) ###################################################################### @@ -303,12 +329,12 @@ def min_value(self) -> Decimal | None: return self.min @min_value.setter - def min_value(self, value): + def min_value(self, value: NumberInputT | None) -> None: warnings.warn( "NumberInput.min_value has been renamed NumberInput.min", DeprecationWarning, ) - self.min = value + self.min = value # type: ignore[assignment] @property def max_value(self) -> Decimal | None: @@ -320,9 +346,9 @@ def max_value(self) -> Decimal | None: return self.max @max_value.setter - def max_value(self, value): + def max_value(self, value: NumberInputT | None) -> None: warnings.warn( "NumberInput.max_value has been renamed NumberInput.max", DeprecationWarning, ) - self.max = value + self.max = value # type: ignore[assignment] diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index ebc6621091..0ad581701a 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -1,38 +1,53 @@ from __future__ import annotations -import sys -from typing import TYPE_CHECKING, Any, Protocol, overload +from typing import ( + TYPE_CHECKING, + Any, + Iterable, + Protocol, + Tuple, + Union, + no_type_check, + overload, +) import toga -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.platform import get_platform_factory +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget if TYPE_CHECKING: - if sys.version_info < (3, 10): - from typing_extensions import TypeAlias - else: - from typing import TypeAlias - from toga.icons import IconContent - OptionContainerContent: TypeAlias = ( - tuple[str, Widget] - | tuple[str, Widget, IconContent | None] - | tuple[str, Widget, IconContent | None, bool] - | toga.OptionItem - ) + OptionContainerContent: TypeAlias = Union[ + Tuple[str, Widget], + Tuple[str, Widget, Union[IconContent, None]], + Tuple[str, Widget, Union[IconContent, None], bool], + toga.OptionItem, + ] -class OnSelectHandler(Protocol): - def __call__(self, widget: OptionContainer, **kwargs: Any) -> None: - """A handler that will be invoked when a new tab is selected in the OptionContainer. +class OnSelectHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the option list is selected.""" + + +class OnSelectHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnSelectHandlerSync`.""" - :param widget: The OptionContainer that had a selection change. - :param kwargs: Ensures compatibility with arguments added in future versions. - """ - ... + +class OnSelectHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnSelectHandlerSync`.""" + + +OnSelectHandlerT: TypeAlias = Union[ + OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator +] class OptionItem: @@ -60,18 +75,18 @@ def __init__( # will become the source of truth. Initially prime the attributes with None (so # that the attribute exists), then use the setter to enforce validation on the # provided values. - self._text = None - self._icon = None - self._enabled = None + self._text: str = None # type:ignore[assignment] + self._icon: toga.Icon = None # type:ignore[assignment] + self._enabled: bool = None # type:ignore[assignment] self.text = text - self.icon = icon + self.icon = icon # type:ignore[assignment] self.enabled = enabled # Prime the attributes for properties that will be set when the OptionItem is # set as content. - self._interface = None - self._index = None + self._interface: OptionContainer = None # type:ignore[assignment] + self._index: int = None # type:ignore[assignment] @property def interface(self) -> OptionContainer: @@ -90,7 +105,7 @@ def enabled(self) -> bool: return self._interface._impl.is_option_enabled(self.index) @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: enable = bool(value) if hasattr(self, "_enabled"): self._enabled = enable @@ -112,7 +127,7 @@ def text(self) -> str: return self._interface._impl.get_option_text(self.index) @text.setter - def text(self, value): + def text(self, value: object) -> None: if value is None: raise ValueError("Item text cannot be None") @@ -140,7 +155,7 @@ def icon(self) -> toga.Icon: return self._interface._impl.get_option_icon(self.index) @icon.setter - def icon(self, icon_or_name: IconContent | None): + def icon(self, icon_or_name: IconContent | None) -> None: if get_platform_factory().OptionContainer.uses_icons: if icon_or_name is None: icon = None @@ -150,7 +165,7 @@ def icon(self, icon_or_name: IconContent | None): icon = toga.Icon(icon_or_name) if hasattr(self, "_icon"): - self._icon = icon + self._icon = icon # type:ignore[assignment] else: self._interface._impl.set_option_icon(self.index, icon) @@ -167,17 +182,17 @@ def content(self) -> Widget: """The content widget displayed in this tab of the OptionContainer.""" return self._content - def _preserve_option(self): + def _preserve_option(self) -> None: # Move the ground truth back to the OptionItem instance self._text = self.text self._icon = self.icon self._enabled = self.enabled # Clear - self._index = None - self._interface = None + self._index = None # type:ignore[assignment] + self._interface = None # type:ignore[assignment] - def _add_as_option(self, index, interface): + def _add_as_option(self, index: int, interface: OptionContainer) -> None: text = self._text del self._text @@ -198,23 +213,23 @@ def _add_as_option(self, index, interface): class OptionList: - def __init__(self, interface): + def __init__(self, interface: Any): self.interface = interface - self._options = [] + self._options: list[OptionItem] = [] - def __repr__(self): - items = ", ".join(repr(option.text) for option in self) + def __repr__(self) -> str: + items = ", ".join(repr(option.text) for option in self) # type: ignore[attr-defined] return f"" def __getitem__(self, index: int | str | OptionItem) -> OptionItem: """Obtain a specific tab of content.""" return self._options[self.index(index)] - def __delitem__(self, index: int | str | OptionItem): + def __delitem__(self, index: int | str | OptionItem) -> None: """Same as :any:`remove`.""" self.remove(index) - def remove(self, index: int | str | OptionItem): + def remove(self, index: int | str | OptionItem) -> None: """Remove the specified tab of content. The currently selected item cannot be deleted. @@ -244,7 +259,7 @@ def __len__(self) -> int: """The number of tabs of content in the OptionContainer.""" return len(self._options) - def index(self, value: str | int | OptionItem): + def index(self, value: str | int | OptionItem) -> int: """Find the index of the tab that matches the given value. :param value: The value to look for. An integer is returned as-is; @@ -256,10 +271,10 @@ def index(self, value: str | int | OptionItem): if isinstance(value, int): return value elif isinstance(value, OptionItem): - return value.index + return value.index # type:ignore[return-value] else: try: - return next(filter(lambda item: item.text == str(value), self)).index + return next(filter(lambda item: item.text == str(value), self)).index # type: ignore except StopIteration: raise ValueError(f"No tab named {value!r}") @@ -267,7 +282,7 @@ def index(self, value: str | int | OptionItem): def append( self, text_or_item: OptionItem, - ): ... + ) -> None: ... @overload def append( @@ -276,17 +291,17 @@ def append( content: Widget, *, icon: IconContent | None = None, - enabled: bool = True, - ): ... + enabled: bool | None = True, + ) -> None: ... def append( self, - text_or_item: str, + text_or_item: str | OptionItem, content: Widget | None = None, *, icon: IconContent | None = None, - enabled: bool = None, - ): + enabled: bool | None = None, + ) -> None: """Add a new tab of content to the OptionContainer. The new tab can be specified as an existing :any:`OptionItem` instance, or by @@ -299,14 +314,20 @@ def append( :param icon: The :any:`icon content ` to use to represent the tab. :param enabled: Should the new tab be enabled? (Default: ``True``) """ - self.insert(len(self), text_or_item, content, icon=icon, enabled=enabled) + self.insert( # type:ignore[misc] + len(self), + text_or_item, # type:ignore[arg-type] + content, # type:ignore[arg-type] + icon=icon, + enabled=enabled, + ) @overload def insert( self, index: int | str | OptionItem, text_or_item: OptionItem, - ): ... + ) -> None: ... @overload def insert( @@ -316,8 +337,8 @@ def insert( content: Widget, *, icon: IconContent | None = None, - enabled: bool = True, - ): ... + enabled: bool | None = True, + ) -> None: ... def insert( self, @@ -327,7 +348,7 @@ def insert( *, icon: IconContent | None = None, enabled: bool | None = None, - ): + ) -> None: """Insert a new tab of content to the OptionContainer at the specified index. The new tab can be specified as an existing :any:`OptionItem` instance, or by @@ -357,7 +378,7 @@ def insert( # Create an interface wrapper for the option. item = OptionItem( text_or_item, - content, + content, # type:ignore[arg-type] icon=icon, enabled=enabled if enabled is not None else True, ) @@ -379,10 +400,10 @@ def insert( class OptionContainer(Widget): def __init__( self, - id=None, - style=None, - content: list[OptionContainerContent] | None = None, - on_select: toga.widgets.optioncontainer.OnSelectHandler | None = None, + id: str | None = None, + style: Pack | None = None, + content: Iterable[tuple[str, Widget]] | None = None, + on_select: OnSelectHandlerT | None = None, ): """Create a new OptionContainer. @@ -395,20 +416,22 @@ def __init__( """ super().__init__(id=id, style=style) self._content = OptionList(self) - self.on_select = None + self.on_select = None # type: ignore[assignment] self._impl = self.factory.OptionContainer(interface=self) if content: for item in content: + # TODO:PR: OptionItem is not a widget...but iterating content returns widgets... if isinstance(item, OptionItem): - self.content.append(item) + self.content.append(item) # type:ignore[unreachable] else: if len(item) == 2: text, widget = item icon = None enabled = True - elif len(item) == 3: + # TODO:PR: type of content is Iterable of tuples soooo.... + elif len(item) == 3: # type:ignore[unreachable] text, widget, icon = item enabled = True elif len(item) == 4: @@ -422,7 +445,7 @@ def __init__( self.content.append(text, widget, enabled=enabled, icon=icon) - self.on_select = on_select + self.on_select = on_select # type: ignore[assignment] @property def enabled(self) -> bool: @@ -434,12 +457,11 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - """No-op; OptionContainer cannot accept input focus""" - pass + def focus(self) -> None: + """No-op; OptionContainer cannot accept input focus.""" @property def content(self) -> OptionList: @@ -459,7 +481,7 @@ def current_tab(self) -> OptionItem | None: return self._content[index] @current_tab.setter - def current_tab(self, value): + def current_tab(self, value: OptionItem | str | int) -> None: index = self._content.index(value) if not self._impl.is_option_enabled(index): raise ValueError("A disabled tab cannot be made the current tab.") @@ -467,7 +489,8 @@ def current_tab(self, value): self._impl.set_current_tab_index(index) @Widget.app.setter - def app(self, app): + @no_type_check + def app(self, app) -> None: # Invoke the superclass property setter Widget.app.fset(self, app) @@ -476,7 +499,8 @@ def app(self, app): item._content.app = app @Widget.window.setter - def window(self, window): + @no_type_check + def window(self, window) -> None: # Invoke the superclass property setter Widget.window.fset(self, window) @@ -485,10 +509,10 @@ def window(self, window): item._content.window = window @property - def on_select(self) -> toga.widgets.optioncontainer.OnSelectHandler: + def on_select(self) -> WrappedHandlerT: """The callback to invoke when a new tab of content is selected.""" return self._on_select @on_select.setter - def on_select(self, handler): + def on_select(self, handler: OnSelectHandlerT) -> None: self._on_select = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/passwordinput.py b/core/src/toga/widgets/passwordinput.py index 8044527841..48548b4935 100644 --- a/core/src/toga/widgets/passwordinput.py +++ b/core/src/toga/widgets/passwordinput.py @@ -6,5 +6,5 @@ class PasswordInput(TextInput): """Create a new password input widget.""" - def _create(self): + 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 31bb1e80e5..e4d1e77a2b 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -1,5 +1,9 @@ from __future__ import annotations +from typing import Literal, SupportsFloat + +from toga.style import Pack + from .base import Widget @@ -8,10 +12,10 @@ class ProgressBar(Widget): def __init__( self, - id=None, - style=None, - max: float = 1.0, - value: float = 0.0, + id: str | None = None, + style: Pack | None = None, + max: str | SupportsFloat = 1.0, + value: str | SupportsFloat = 0.0, running: bool = False, ): """Create a new Progress Bar widget. @@ -32,14 +36,14 @@ def __init__( self._impl = self.factory.ProgressBar(interface=self) - self.max = max - self.value = value + self.max = max # type: ignore[assignment] + self.value = value # type: ignore[assignment] if running: self.start() - @property - def enabled(self) -> bool: + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? ProgressBar widgets cannot be disabled; this property will always return True; @@ -48,7 +52,7 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass @property @@ -69,7 +73,7 @@ def is_determinate(self) -> bool: """ return self.max is not None - def start(self): + def start(self) -> None: """Start the progress bar. If the progress bar is already started, this is a no-op. @@ -77,7 +81,7 @@ def start(self): if not self.is_running: self._impl.start() - def stop(self): + def stop(self) -> None: """Stop the progress bar. If the progress bar is already stopped, this is a no-op. @@ -98,7 +102,7 @@ def value(self) -> float: return self._impl.get_value() @value.setter - def value(self, value): + def value(self, value: str | SupportsFloat) -> None: if self.max is not None: value = max(0.0, min(self.max, float(value))) self._impl.set_value(value) @@ -112,7 +116,7 @@ def max(self) -> float | None: return self._impl.get_max() @max.setter - def max(self, value): + def max(self, value: str | SupportsFloat | None) -> None: if value is None: self._impl.set_max(None) elif float(value) > 0.0: diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index f1e5dc2676..a929c0dd39 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -1,18 +1,42 @@ from __future__ import annotations -from toga.handlers import wrapped_handler +from typing import Literal, Protocol, SupportsInt, Union, no_type_check + +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +class OnScrollHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the container is scrolled.""" + + +class OnScrollHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnScrollHandlerSync`.""" + + +class OnScrollHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnScrollHandlerSync`.""" + + +OnScrollHandlerT: TypeAlias = Union[ + OnScrollHandlerSync, OnScrollHandlerAsync, OnScrollHandlerGenerator +] + + class ScrollContainer(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, horizontal: bool = True, vertical: bool = True, - on_scroll: callable | None = None, + on_scroll: OnScrollHandlerT | None = None, content: Widget | None = None, ): """Create a new Scroll Container. @@ -27,8 +51,9 @@ def __init__( """ super().__init__(id=id, style=style) - self._content = None - self.on_scroll = None + self._content: Widget | None = None + self.on_scroll = None # type: ignore[assignment] + # Create a platform specific implementation of a Scroll Container self._impl = self.factory.ScrollContainer(interface=self) @@ -36,10 +61,11 @@ def __init__( self.vertical = vertical self.horizontal = horizontal self.content = content - self.on_scroll = on_scroll + self.on_scroll = on_scroll # type: ignore[assignment] @Widget.app.setter - def app(self, app): + @no_type_check + def app(self, app) -> None: # Invoke the superclass property setter Widget.app.fset(self, app) @@ -48,7 +74,8 @@ def app(self, app): self._content.app = app @Widget.window.setter - def window(self, window): + @no_type_check + def window(self, window) -> None: # Invoke the superclass property setter Widget.window.fset(self, window) @@ -56,8 +83,8 @@ def window(self, window): if self._content: self._content.window = window - @property - def enabled(self) -> bool: + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? ScrollContainer widgets cannot be disabled; this property will always return @@ -66,20 +93,20 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; ScrollContainer cannot accept input focus" + def focus(self) -> None: + """No-op; ScrollContainer cannot accept input focus.""" pass @property - def content(self) -> Widget: + def content(self) -> Widget | None: """The root content widget displayed inside the scroll container.""" return self._content @content.setter - def content(self, widget): + def content(self, widget: Widget | None) -> None: if self._content: # Clear the window before the app so that registry entries can be cleared self._content.window = None @@ -102,7 +129,7 @@ def vertical(self) -> bool: return self._impl.get_vertical() @vertical.setter - def vertical(self, value): + def vertical(self, value: object) -> None: self._impl.set_vertical(bool(value)) if self._content: self._content.refresh() @@ -113,18 +140,18 @@ def horizontal(self) -> bool: return self._impl.get_horizontal() @horizontal.setter - def horizontal(self, value): + def horizontal(self, value: object) -> None: self._impl.set_horizontal(bool(value)) if self._content: self._content.refresh() @property - def on_scroll(self) -> callable: + def on_scroll(self) -> WrappedHandlerT: """Handler to invoke when the user moves a scroll bar.""" return self._on_scroll @on_scroll.setter - def on_scroll(self, on_scroll): + def on_scroll(self, on_scroll: OnScrollHandlerT) -> None: self._on_scroll = wrapped_handler(self, on_scroll) @property @@ -149,13 +176,13 @@ def horizontal_position(self) -> int: return self._impl.get_horizontal_position() @horizontal_position.setter - def horizontal_position(self, horizontal_position): + def horizontal_position(self, horizontal_position: SupportsInt) -> None: if not self.horizontal: raise ValueError( "Cannot set horizontal position when horizontal scrolling is not enabled." ) - self.position = (horizontal_position, self._impl.get_vertical_position()) + self.position = (horizontal_position, self._impl.get_vertical_position()) # type: ignore[assignment] @property def max_vertical_position(self) -> int: @@ -179,13 +206,13 @@ def vertical_position(self) -> int: return self._impl.get_vertical_position() @vertical_position.setter - def vertical_position(self, vertical_position): + def vertical_position(self, vertical_position: SupportsInt) -> None: if not self.vertical: raise ValueError( "Cannot set vertical position when vertical scrolling is not enabled." ) - self.position = (self._impl.get_horizontal_position(), vertical_position) + self.position = (self._impl.get_horizontal_position(), vertical_position) # type: ignore[assignment] # This combined property is necessary because on some platforms (e.g. iOS), setting # the horizontal and vertical position separately would cause the horizontal and @@ -200,10 +227,10 @@ def position(self) -> tuple[int, int]: If scrolling is disabled in either axis, the value provided for that axis will be ignored. """ - return (self.horizontal_position, self.vertical_position) + return self.horizontal_position, self.vertical_position @position.setter - def position(self, position): + def position(self, position: tuple[SupportsInt, SupportsInt]) -> None: horizontal_position, vertical_position = map(int, position) if self.horizontal: if horizontal_position < 0: diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 83a9ae3bf1..2e65a3b635 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -1,24 +1,50 @@ from __future__ import annotations import warnings +from typing import Any, Generic, Iterable, Protocol, TypeVar, Union -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Source +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +T = TypeVar("T") +SourceT = TypeVar("SourceT", bound=Source) -class Selection(Widget): + +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the value is changed.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator +] + + +class Selection(Widget, Generic[T]): def __init__( self, - id=None, - style=None, - items: list | ListSource | None = None, + id: str | None = None, + style: Pack | None = None, + items: SourceT | Iterable[T] | None = None, accessor: str | None = None, - value: None = None, - on_change: callable | None = None, - enabled=True, - on_select: callable | None = None, # DEPRECATED + value: T | None = None, + on_change: OnChangeHandlerT | None = None, + enabled: bool = True, + on_select: None = None, # DEPRECATED ): """Create a new Selection widget. @@ -40,7 +66,7 @@ def __init__( # 2023-05: Backwards compatibility ###################################################################### if on_select: # pragma: no cover - if on_change: + if on_change: # type: ignore[unreachable] raise ValueError("Cannot specify both on_select and on_change") else: warnings.warn( @@ -52,19 +78,22 @@ def __init__( # End backwards compatibility. ###################################################################### - self.on_change = None # needed for _impl initialization + self._items: SourceT | ListSource[T] + self._on_change: WrappedHandlerT + + self.on_change = None # type: ignore[assignment] # needed for _impl initialization self._impl = self.factory.Selection(interface=self) self._accessor = accessor - self.items = items + self.items = items # type: ignore[assignment] if value: self.value = value - self.on_change = on_change + self.on_change = on_change # type: ignore[assignment] self.enabled = enabled @property - def items(self) -> ListSource: + def items(self) -> SourceT | ListSource[T]: """The items to display in the selection. When setting this property: @@ -81,7 +110,7 @@ def items(self) -> ListSource: return self._items @items.setter - def items(self, items): + def items(self, items: SourceT | Iterable[T] | None) -> None: if self._accessor is None: accessors = ["value"] else: @@ -92,7 +121,7 @@ def items(self, items): elif isinstance(items, Source): if self._accessor is None: raise ValueError("Must specify an accessor to use a data source") - self._items = items + self._items = items # type: ignore[assignment] else: self._items = ListSource(accessors=accessors, data=items) @@ -100,11 +129,11 @@ def items(self, items): # Temporarily halt notifications orig_on_change = self._on_change - self.on_change = None + self.on_change = None # type: ignore[assignment] # Clear the widget, and insert all the data rows self._impl.clear() - for index, item in enumerate(self.items): + for index, item in enumerate(self.items): # type: ignore[arg-type,var-annotated] self._impl.insert(index, item) # Restore the original change handler and trigger it. @@ -113,7 +142,7 @@ def items(self, items): self.refresh() - def _title_for_item(self, item): + def _title_for_item(self, item: Any) -> str: """Internal utility method; return the display title for an item""" if self._accessor: title = getattr(item, self._accessor) @@ -123,7 +152,7 @@ def _title_for_item(self, item): return str(title).split("\n")[0] @property - def value(self): + def value(self) -> T | None: """The currently selected item. Returns None if there are no items in the selection. @@ -143,34 +172,34 @@ def value(self): if index is None: return None - item = self._items[index] + item = self._items[index] # type: ignore[index] # If there was no accessor specified, the data values are literals. # Dereference the value out of the Row object. if item and self._accessor is None: - return item.value - return item + return item.value # type: ignore[union-attr] + return item # type: ignore[return-value] @value.setter - def value(self, value): + def value(self, value: T) -> None: try: if self._accessor is None: - item = self._items.find(dict(value=value)) + item = self._items.find(dict(value=value)) # type: ignore[union-attr] else: item = value - index = self._items.index(item) + index = self._items.index(item) # type: ignore[union-attr] self._impl.select_item(index=index, item=item) except ValueError: raise ValueError(f"{value!r} is not a current item in the selection") @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """Handler to invoke when the value of the selection is changed, either by the user or programmatically.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) ###################################################################### @@ -178,7 +207,7 @@ def on_change(self, handler): ###################################################################### @property - def on_select(self) -> callable: + def on_select(self) -> WrappedHandlerT: """**DEPRECATED**: Use ``on_change``""" warnings.warn( "Selection.on_select has been renamed Selection.on_change.", @@ -187,7 +216,7 @@ def on_select(self) -> callable: return self.on_change @on_select.setter - def on_select(self, handler): + def on_select(self, handler: OnChangeHandlerT) -> None: warnings.warn( "Selection.on_select has been renamed Selection.on_change.", DeprecationWarning, diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 98e559483f..b2809ec496 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -3,26 +3,89 @@ import warnings from abc import ABC, abstractmethod from contextlib import contextmanager +from typing import Any, Iterator, Protocol, SupportsFloat, Union -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the value is changed.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator +] + + +class OnPressHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the slider is pressed.""" + + +class OnPressHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnPressHandlerSync`.""" + + +class OnPressHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnPressHandlerSync`.""" + + +OnPressHandlerT: TypeAlias = Union[ + OnPressHandlerSync, OnPressHandlerAsync, OnPressHandlerGenerator +] + + +class OnReleaseHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the slider is pressed.""" + + +class OnReleaseHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnReleaseHandlerSync`.""" + + +class OnReleaseHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnReleaseHandlerSync`.""" + + +OnReleaseHandlerT: TypeAlias = Union[ + OnReleaseHandlerSync, OnReleaseHandlerAsync, OnReleaseHandlerGenerator +] + + class Slider(Widget): def __init__( self, id: str | None = None, - style=None, + style: Pack | None = None, value: float | None = None, - min: float = None, # Default to 0.0 when range is removed - max: float = None, # Default to 1.0 when range is removed + min: float | None = None, # Default to 0.0 when range is removed + max: float | None = None, # Default to 1.0 when range is removed tick_count: int | None = None, - on_change: callable | None = None, - on_press: callable | None = None, - on_release: callable | None = None, + on_change: OnChangeHandlerT | None = None, + on_press: OnPressHandlerT | None = None, + on_release: OnReleaseHandlerT | None = None, enabled: bool = True, - range: tuple[float, float] | None = None, # DEPRECATED + range: None = None, # DEPRECATED ): """Create a new Slider widget. @@ -49,7 +112,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if range is not None: - if min is not None or max is not None: + if min is not None or max is not None: # type: ignore[unreachable] raise ValueError( "range cannot be specified if min and max are specified" ) @@ -69,9 +132,11 @@ def __init__( # End backwards compatibility ###################################################################### + self._on_change: WrappedHandlerT + # Set a dummy handler before installing the actual on_change, because we do not want # on_change triggered by the initial value being set - self.on_change = None + self.on_change = None # type: ignore[assignment] self.min = min self.max = max self.tick_count = tick_count @@ -79,9 +144,9 @@ def __init__( value = (min + max) / 2 self.value = value - self.on_change = on_change - self.on_press = on_press - self.on_release = on_release + self.on_change = on_change # type: ignore[assignment] + self.on_press = on_press # type: ignore[assignment] + self.on_release = on_release # type: ignore[assignment] self.enabled = enabled @@ -90,10 +155,10 @@ def __init__( # Backends are inconsistent about when they produce events for programmatic changes, # so we deal with those in the interface layer. @contextmanager - def _programmatic_change(self): + def _programmatic_change(self) -> Iterator[float]: old_value = self.value on_change = self._on_change - self.on_change = None + self.on_change = None # type: ignore[assignment] yield old_value self._on_change = on_change @@ -111,7 +176,7 @@ def value(self) -> float: return self._impl.get_value() @value.setter - def value(self, value): + def value(self, value: float) -> None: if value < self.min: value = self.min elif value > self.max: @@ -119,10 +184,10 @@ def value(self, value): with self._programmatic_change(): self._set_value(value) - def _set_value(self, value): + def _set_value(self, value: SupportsFloat) -> None: self._impl.set_value(self._round_value(float(value))) - def _round_value(self, value): + def _round_value(self, value: float) -> float: step = self.tick_step if step is not None: # Round to the nearest tick. @@ -139,7 +204,7 @@ def min(self) -> float: return self._impl.get_min() @min.setter - def min(self, value): + def min(self, value: SupportsFloat) -> None: with self._programmatic_change() as old_value: # Some backends will clip the current value within the range automatically, # but do it ourselves to be certain. In discrete mode, setting self.value also @@ -163,7 +228,7 @@ def max(self) -> float: return self._impl.get_max() @max.setter - def max(self, value): + def max(self, value: SupportsFloat) -> None: with self._programmatic_change() as old_value: # Some backends will clip the current value within the range automatically, # but do it ourselves to be certain. In discrete mode, setting self.value also @@ -199,7 +264,7 @@ def tick_count(self) -> int | None: return self._impl.get_tick_count() @tick_count.setter - def tick_count(self, tick_count): + def tick_count(self, tick_count: float | None) -> None: if (tick_count is not None) and (tick_count < 2): raise ValueError("tick count must be at least 2") with self._programmatic_change() as old_value: @@ -226,7 +291,7 @@ def tick_step(self) -> float | None: return (self.max - self.min) / (self.tick_count - 1) @property - def tick_value(self) -> int | None: + def tick_value(self) -> float | None: """Value of the slider, measured in ticks. * If the slider is continuous, this property returns ``None``. @@ -235,25 +300,25 @@ def tick_value(self) -> int | None: :raises ValueError: If set to anything inconsistent with the rules above. """ - if self.tick_count is not None: + if self.tick_count is not None and self.tick_step is not None: return round((self.value - self.min) / self.tick_step) + 1 else: return None @tick_value.setter - def tick_value(self, tick_value): + def tick_value(self, tick_value: int | None) -> None: if self.tick_count is None: if tick_value is not None: raise ValueError("cannot set tick value when tick count is None") else: - if tick_value is None: + if tick_value is None or self.tick_step is None: raise ValueError( "cannot set tick value to None when tick count is not None" ) self.value = self.min + (tick_value - 1) * self.tick_step @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """Handler to invoke when the value of the slider is changed, either by the user or programmatically. @@ -262,25 +327,25 @@ def on_change(self) -> callable: return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) @property - def on_press(self) -> callable: + def on_press(self) -> WrappedHandlerT: """Handler to invoke when the user presses the slider before changing it.""" return self._on_press @on_press.setter - def on_press(self, handler): + def on_press(self, handler: OnPressHandlerT) -> None: self._on_press = wrapped_handler(self, handler) @property - def on_release(self) -> callable: + def on_release(self) -> WrappedHandlerT: """Handler to invoke when the user releases the slider after changing it.""" return self._on_release @on_release.setter - def on_release(self, handler): + def on_release(self, handler: OnReleaseHandlerT) -> None: self._on_release = wrapped_handler(self, handler) ###################################################################### @@ -303,10 +368,10 @@ def range(self) -> tuple[float, float]: "Slider.range has been deprecated in favor of Slider.min and Slider.max", DeprecationWarning, ) - return (self.min, self.max) + return self.min, self.max @range.setter - def range(self, range): + def range(self, range: tuple[float, float]) -> None: warnings.warn( "Slider.range has been deprecated in favor of Slider.min and Slider.max", DeprecationWarning, @@ -317,29 +382,31 @@ def range(self, range): class SliderImpl(ABC): + interface: Any + @abstractmethod - def get_value(self): ... + def get_value(self) -> float: ... @abstractmethod - def set_value(self, value): ... + def set_value(self, value: float) -> None: ... @abstractmethod - def get_min(self): ... + def get_min(self) -> float: ... @abstractmethod - def set_min(self, value): ... + def set_min(self, value: float) -> None: ... @abstractmethod - def get_max(self): ... + def get_max(self) -> float: ... @abstractmethod - def set_max(self, value): ... + def set_max(self, value: float) -> None: ... @abstractmethod - def get_tick_count(self): ... + def get_tick_count(self) -> int | None: ... @abstractmethod - def set_tick_count(self, tick_count): ... + def set_tick_count(self, tick_count: int | None) -> None: ... class IntSliderImpl(SliderImpl): @@ -348,41 +415,41 @@ class IntSliderImpl(SliderImpl): # Number of steps to use to approximate a continuous slider. CONTINUOUS_MAX = 10000 - def __init__(self): + def __init__(self) -> None: super().__init__() # Dummy values used during initialization. - self.value = 0 - self.min = 0 - self.max = 1 + self.value: int | float = 0 + self.min: int | float = 0 + self.max: int | float = 1 self.discrete = False - def get_value(self): + def get_value(self) -> float: return self.value - def set_value(self, value): + def set_value(self, value: float) -> None: span = self.max - self.min self.set_int_value( 0 if span == 0 else round((value - self.min) / span * self.get_int_max()) ) self.value = value # Cache the original value so we can round-trip it. - def get_min(self): + def get_min(self) -> float: return self.min - def set_min(self, value): + def set_min(self, value: float) -> None: self.min = value - def get_max(self): + def get_max(self) -> float: return self.max - def set_max(self, value): + def set_max(self, value: float) -> None: self.max = value - def get_tick_count(self): + def get_tick_count(self) -> int | None: return (self.get_int_max() + 1) if self.discrete else None - def set_tick_count(self, tick_count): + def set_tick_count(self, tick_count: int | None) -> None: if tick_count is None: self.discrete = False self.set_int_max(self.CONTINUOUS_MAX) @@ -393,22 +460,22 @@ def set_tick_count(self, tick_count): # Instead of calling the event handler directly, implementations should call this # method. - def on_change(self): + def on_change(self) -> None: span = self.max - self.min self.value = self.min + (self.get_int_value() / self.get_int_max() * span) self.interface.on_change() @abstractmethod - def get_int_value(self): ... + def get_int_value(self) -> int: ... @abstractmethod - def set_int_value(self, value): ... + def set_int_value(self, value: int) -> None: ... @abstractmethod - def get_int_max(self): ... + def get_int_max(self) -> int: ... @abstractmethod - def set_int_max(self, max): ... + def set_int_max(self, max: int) -> None: ... @abstractmethod - def set_ticks_visible(self, visible): ... + def set_ticks_visible(self, visible: bool) -> None: ... diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 79b29e3789..9be667f4f2 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -1,9 +1,15 @@ from __future__ import annotations +from typing import Tuple, Union, no_type_check + from toga.constants import Direction +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +ContentT: TypeAlias = Union[Widget, Tuple[Widget, float], None] + class SplitContainer(Widget): HORIZONTAL = Direction.HORIZONTAL @@ -11,10 +17,10 @@ class SplitContainer(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, direction: Direction = Direction.VERTICAL, - content: tuple[Widget | None | tuple, Widget | None | tuple] = (None, None), + content: tuple[ContentT, ContentT] = (None, None), ): """Create a new SplitContainer. @@ -29,7 +35,7 @@ def __init__( being empty. """ super().__init__(id=id, style=style) - self._content = (None, None) + self._content: tuple[ContentT, ContentT] = (None, None) # Create a platform specific implementation of a SplitContainer self._impl = self.factory.SplitContainer(interface=self) @@ -47,17 +53,15 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; SplitContainer cannot accept input focus" + def focus(self) -> None: + """No-op; SplitContainer cannot accept input focus.""" pass - # The inner tuple's full type is tuple[Widget | None, float], but that would make - # the documentation unreadable. @property - def content(self) -> tuple[Widget | None | tuple, Widget | None | tuple]: + def content(self) -> tuple[ContentT, ContentT]: """The widgets displayed in the SplitContainer. This property accepts a sequence of exactly 2 elements, each of which can be @@ -75,7 +79,7 @@ def content(self) -> tuple[Widget | None | tuple, Widget | None | tuple]: return self._content @content.setter - def content(self, content): + def content(self, content: tuple[ContentT, ContentT]) -> None: try: if len(content) != 2: raise TypeError() @@ -115,10 +119,11 @@ def content(self, content): tuple(w._impl if w is not None else None for w in _content), flex, ) - self._content = tuple(_content) + self._content = tuple(_content) # type: ignore[assignment] self.refresh() @Widget.app.setter + @no_type_check def app(self, app): # Invoke the superclass property setter Widget.app.fset(self, app) @@ -129,6 +134,7 @@ def app(self, app): content.app = app @Widget.window.setter + @no_type_check def window(self, window): # Invoke the superclass property setter Widget.window.fset(self, window) @@ -144,6 +150,6 @@ def direction(self) -> Direction: return self._impl.get_direction() @direction.setter - def direction(self, value): + def direction(self, value: object) -> None: self._impl.set_direction(value) self.refresh() diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index b5aa934603..7f0f1c53ab 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -1,17 +1,41 @@ from __future__ import annotations -from toga.handlers import wrapped_handler +from typing import Protocol, Union + +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the value is changed.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator +] + + class Switch(Widget): def __init__( self, - text, - id=None, - style=None, - on_change: callable | None = None, + text: str, + id: str | None = None, + style: Pack | None = None, + on_change: OnChangeHandlerT | None = None, value: bool = False, enabled: bool = True, ): @@ -35,10 +59,10 @@ def __init__( # Set a dummy handler before installing the actual on_change, because we do not want # on_change triggered by the initial value being set - self.on_change = None + self.on_change = None # type: ignore[assignment] self.value = value - self.on_change = on_change + self.on_change = on_change # type: ignore[assignment] self.enabled = enabled @@ -56,7 +80,7 @@ def text(self) -> str: return self._impl.get_text() @text.setter - def text(self, value): + def text(self, value: object) -> None: if value is None or value == "\u200B": value = "" else: @@ -68,12 +92,12 @@ def text(self, value): self.refresh() @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """The handler to invoke when the value of the switch changes.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) @property @@ -85,9 +109,9 @@ def value(self) -> bool: return self._impl.get_value() @value.setter - def value(self, value): + def value(self, value: object) -> None: self._impl.set_value(bool(value)) - def toggle(self): + def toggle(self) -> None: """Reverse the current value of the switch.""" self.value = not self.value diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index c63de105c6..d29e54290d 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -1,28 +1,73 @@ from __future__ import annotations import warnings -from typing import Any +from typing import Any, Generic, Iterable, Literal, Protocol, TypeVar, Union -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source from toga.sources.accessors import build_accessors, to_accessor +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +T = TypeVar("T") +SourceT = TypeVar("SourceT", bound=Source) -class Table(Widget): + +class OnSelectHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the table is selected.""" + + +class OnSelectHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnSelectHandlerSync`.""" + + +class OnSelectHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnSelectHandlerSync`.""" + + +OnSelectHandlerT: TypeAlias = Union[ + OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator +] + + +class OnActivateHandlerSync(Protocol): + def __call__(self, row: Any, /) -> object: + """A handler to invoke when the table is activated.""" + + +class OnActivateHandlerAsync(Protocol): + async def __call__(self, row: Any, /) -> object: + """Async definition of :any:`OnActivateHandlerSync`.""" + + +class OnActivateHandlerGenerator(Protocol): + def __call__(self, row: Any, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnActivateHandlerSync`.""" + + +OnActivateHandlerT: TypeAlias = Union[ + OnActivateHandlerSync, OnActivateHandlerAsync, OnActivateHandlerGenerator +] + + +class Table(Widget, Generic[T]): def __init__( self, - headings: list[str] | None = None, - id=None, - style=None, - data: Any = None, + headings: Iterable[str] | None = None, + id: str | None = None, + style: Pack | None = None, + data: SourceT | Iterable[T] | None = None, accessors: list[str] | None = None, multiple_select: bool = False, - on_select: callable | None = None, - on_activate: callable | None = None, + on_select: OnSelectHandlerT | None = None, + on_activate: OnActivateHandlerT | None = None, missing_value: str = "", - on_double_click=None, # DEPRECATED + on_double_click: None = None, # DEPRECATED ): """Create a new Table widget. @@ -61,7 +106,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if on_double_click: - if on_activate: + if on_activate: # type: ignore[unreachable] raise ValueError("Cannot specify both on_double_click and on_activate") else: warnings.warn( @@ -73,6 +118,9 @@ def __init__( # End backwards compatibility. ###################################################################### + self._headings: list[str] | None + self._data: SourceT | ListSource[T] + if headings is not None: self._headings = [heading.split("\n")[0] for heading in headings] self._accessors = build_accessors(self._headings, accessors) @@ -88,18 +136,18 @@ def __init__( self._missing_value = missing_value or "" # Prime some properties that need to exist before the table is created. - self.on_select = None - self.on_activate = None - self._data = None + self.on_select = None # type: ignore[assignment] + self.on_activate = None # type: ignore[assignment] + self._data = None # type: ignore[assignment] self._impl = self.factory.Table(interface=self) - self.data = data + self.data = data # type: ignore[assignment] - self.on_select = on_select - self.on_activate = on_activate + self.on_select = on_select # type: ignore[assignment] + self.on_activate = on_activate # type: ignore[assignment] - @property - def enabled(self) -> bool: + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? Table widgets cannot be disabled; this property will always return True; any attempt to modify it will be ignored. @@ -107,15 +155,15 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; Table cannot accept input focus" + def focus(self) -> None: + """No-op; Table cannot accept input focus.""" pass @property - def data(self) -> ListSource: + def data(self) -> ListSource[T]: """The data to display in the table. When setting this property: @@ -128,14 +176,14 @@ def data(self) -> ListSource: * Otherwise, the value must be an iterable, which is copied into a new ListSource. Items are converted as shown :ref:`here `. """ - return self._data + return self._data # type: ignore[return-value] @data.setter - def data(self, data: Any): + def data(self, data: SourceT | Iterable[T] | None) -> None: if data is None: self._data = ListSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): - self._data = data + self._data = data # type: ignore[assignment] else: self._data = ListSource(accessors=self._accessors, data=data) @@ -148,7 +196,7 @@ def multiple_select(self) -> bool: return self._multiple_select @property - def selection(self) -> list[Row] | Row | None: + def selection(self) -> list[Row[T]] | Row[T] | None: """The current selection of the table. If multiple selection is enabled, returns a list of Row objects from the data @@ -166,11 +214,11 @@ def selection(self) -> list[Row] | Row | None: else: return self.data[selection] - def scroll_to_top(self): + def scroll_to_top(self) -> None: """Scroll the view so that the top of the list (first row) is visible.""" self.scroll_to_row(0) - def scroll_to_row(self, row: int): + def scroll_to_row(self, row: int) -> None: """Scroll the view so that the specified row index is visible. :param row: The index of the row to make visible. Negative values refer to the @@ -182,34 +230,34 @@ def scroll_to_row(self, row: int): else: self._impl.scroll_to_row(max(len(self.data) + row, 0)) - def scroll_to_bottom(self): + def scroll_to_bottom(self) -> None: """Scroll the view so that the bottom of the list (last row) is visible.""" self.scroll_to_row(-1) @property - def on_select(self) -> callable: + def on_select(self) -> WrappedHandlerT: """The callback function that is invoked when a row of the table is selected.""" return self._on_select @on_select.setter - def on_select(self, handler: callable): + def on_select(self, handler: OnSelectHandlerT) -> None: self._on_select = wrapped_handler(self, handler) @property - def on_activate(self) -> callable: + def on_activate(self) -> WrappedHandlerT: """The callback function that is invoked when a row of the table is activated, usually with a double click or similar action.""" return self._on_activate @on_activate.setter - def on_activate(self, handler): + def on_activate(self, handler: OnActivateHandlerT) -> None: self._on_activate = wrapped_handler(self, handler) - def add_column(self, heading: str, accessor: str | None = None): + def add_column(self, heading: str, accessor: str | None = None) -> None: """**DEPRECATED**: use :meth:`~toga.Table.append_column`""" self.insert_column(len(self._accessors), heading, accessor=accessor) - def append_column(self, heading: str, accessor: str | None = None): + def append_column(self, heading: str, accessor: str | None = None) -> None: """Append a column to the end of the table. :param heading: The heading for the new column. @@ -222,9 +270,9 @@ def append_column(self, heading: str, accessor: str | None = None): def insert_column( self, index: int | str, - heading: str | None, + heading: str, accessor: str | None = None, - ): + ) -> None: """Insert an additional column into the table. :param index: The index at which to insert the column, or the accessor of the @@ -238,7 +286,7 @@ def insert_column( if self._headings is None: if accessor is None: raise ValueError("Must specify an accessor on a table without headings") - heading = None + heading = None # type: ignore[assignment] elif not accessor: accessor = to_accessor(heading) @@ -257,7 +305,7 @@ def insert_column( self._impl.insert_column(index, heading, accessor) - def remove_column(self, column: int | str): + def remove_column(self, column: int | str) -> None: """Remove a table column. :param column: The index of the column to remove, or the accessor of the column @@ -302,7 +350,7 @@ def missing_value(self) -> str: ###################################################################### @property - def on_double_click(self): + def on_double_click(self) -> WrappedHandlerT: """**DEPRECATED**: Use ``on_activate``""" warnings.warn( "Table.on_double_click has been renamed Table.on_activate.", @@ -311,7 +359,7 @@ def on_double_click(self): return self.on_activate @on_double_click.setter - def on_double_click(self, handler): + def on_double_click(self, handler: OnActivateHandlerT) -> None: warnings.warn( "Table.on_double_click has been renamed Table.on_activate.", DeprecationWarning, diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 5c207e7edc..53b226709e 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -1,70 +1,153 @@ from __future__ import annotations -from toga.handlers import wrapped_handler +from typing import Callable, Protocol, Union + +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the text input is changed.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator +] + + +class OnConfirmHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the text input is confirmed.""" + + +class OnConfirmHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnConfirmHandlerSync`.""" + + +class OnConfirmHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnConfirmHandlerSync`.""" + + +OnConfirmHandlerT: TypeAlias = Union[ + OnConfirmHandlerSync, OnConfirmHandlerAsync, OnConfirmHandlerGenerator +] + + +class OnGainFocusHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the text input gains focus.""" + + +class OnGainFocusHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnGainFocusHandlerSync`.""" + + +class OnGainFocusHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnGainFocusHandlerSync`.""" + + +OnGainFocusHandlerT: TypeAlias = Union[ + OnGainFocusHandlerSync, OnGainFocusHandlerAsync, OnGainFocusHandlerGenerator +] + + +class OnLoseFocusHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the text input loses focus.""" + + +class OnLoseFocusHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnLoseFocusHandlerSync`.""" + + +class OnLoseFocusHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnLoseFocusHandlerSync`.""" + + +OnLoseFocusHandlerT: TypeAlias = Union[ + OnLoseFocusHandlerSync, OnLoseFocusHandlerAsync, OnLoseFocusHandlerGenerator +] + + class TextInput(Widget): """Create a new single-line text input widget.""" def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, value: str | None = None, readonly: bool = False, placeholder: str | None = None, - on_change: callable | None = None, - on_confirm: callable | None = None, - on_gain_focus: callable | None = None, - on_lose_focus: callable | None = None, - validators: list[callable] | None = None, + on_change: OnChangeHandlerT | None = None, + on_confirm: OnConfirmHandlerT | None = None, + on_gain_focus: OnGainFocusHandlerT | None = None, + on_lose_focus: OnLoseFocusHandlerT | None = None, + validators: list[Callable[[str], bool]] | None = None, ): """ :param id: The ID for the widget. - :param style: A style object. If no style is provided, a default style - will be applied to the widget. + :param style: A style object. If no style is provided, a default style will be + applied to the widget. :param value: The initial content to display in the widget. :param readonly: Can the value of the widget be modified by the user? - :param placeholder: The content to display as a placeholder when there - is no user content to display. - :param on_change: A handler that will be invoked when the value of - the widget changes. - :param on_confirm: A handler that will be invoked when the user accepts - the value of the input (usually by pressing Return on the keyboard). - :param on_gain_focus: A handler that will be invoked when the widget - gains input focus. - :param on_lose_focus: A handler that will be invoked when the widget - loses input focus. - :param validators: A list of validators to run on the value of the - input. + :param placeholder: The content to display as a placeholder when there is no + user content to display. + :param on_change: A handler that will be invoked when the value of the widget + changes. + :param on_confirm: A handler that will be invoked when the user accepts the + value of the input (usually by pressing Return on the keyboard). + :param on_gain_focus: A handler that will be invoked when the widget gains + input focus. + :param on_lose_focus: A handler that will be invoked when the widget loses + input focus. + :param validators: A list of validators to run on the value of the input. """ super().__init__(id=id, style=style) # Create a platform specific implementation of the widget self._create() - self.placeholder = placeholder + self.placeholder = placeholder # type: ignore[assignment] self.readonly = readonly # Set the actual value before on_change, because we do not want # on_change triggered by it However, we need to prime the handler # property in case it is accessed. - self.on_change = None - self.on_confirm = None + self.on_change = None # type: ignore[assignment] + self.on_confirm = None # type: ignore[assignment] # Set the list of validators before we set the initial value so that # validation is performed on the initial value - self.validators = validators - self.value = value + self.validators = validators # type: ignore[assignment] + self.value = value # type: ignore[assignment] - self.on_change = on_change - self.on_confirm = on_confirm - self.on_lose_focus = on_lose_focus - self.on_gain_focus = on_gain_focus + self.on_change = on_change # type: ignore[assignment] + self.on_confirm = on_confirm # type: ignore[assignment] + self.on_lose_focus = on_lose_focus # type: ignore[assignment] + self.on_gain_focus = on_gain_focus # type: ignore[assignment] - def _create(self): + def _create(self) -> None: self._impl = self.factory.TextInput(interface=self) @property @@ -78,7 +161,7 @@ def readonly(self) -> bool: return self._impl.get_readonly() @readonly.setter - def readonly(self, value): + def readonly(self, value: object) -> None: self._impl.set_readonly(bool(value)) @property @@ -91,7 +174,7 @@ def placeholder(self) -> str: return self._impl.get_placeholder() @placeholder.setter - def placeholder(self, value): + def placeholder(self, value: object) -> None: self._impl.set_placeholder("" if value is None else str(value)) self.refresh() @@ -109,7 +192,7 @@ def value(self) -> str: return self._impl.get_value() @value.setter - def value(self, value: str): + def value(self, value: object) -> None: if value is None: v = "" else: @@ -123,16 +206,16 @@ def is_valid(self) -> bool: return self._impl.is_valid() @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """The handler to invoke when the value of the widget changes.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) @property - def validators(self) -> list[callable]: + def validators(self) -> list[Callable[[str], bool]]: """The list of validators being used to check input on the widget. Changing the list of validators will cause validation to be performed. @@ -140,7 +223,7 @@ def validators(self) -> list[callable]: return self._validators @validators.setter - def validators(self, validators): + def validators(self, validators: list[Callable[[str], bool]] | None) -> None: replacing = hasattr(self, "_validators") if validators is None: self._validators = [] @@ -151,24 +234,24 @@ def validators(self, validators): self._validate() @property - def on_gain_focus(self) -> callable: + def on_gain_focus(self) -> WrappedHandlerT: """The handler to invoke when the widget gains input focus.""" return self._on_gain_focus @on_gain_focus.setter - def on_gain_focus(self, handler): + def on_gain_focus(self, handler: OnGainFocusHandlerT) -> None: self._on_gain_focus = wrapped_handler(self, handler) @property - def on_lose_focus(self) -> callable: + def on_lose_focus(self) -> WrappedHandlerT: """The handler to invoke when the widget loses input focus.""" return self._on_lose_focus @on_lose_focus.setter - def on_lose_focus(self, handler): + def on_lose_focus(self, handler: OnLoseFocusHandlerT) -> None: self._on_lose_focus = wrapped_handler(self, handler) - def _validate(self): + def _validate(self) -> None: """Validate the current value of the widget. If a problem is found, the widget will be put into an error state. @@ -187,12 +270,12 @@ def _value_changed(self): self.on_change() @property - def on_confirm(self) -> callable: + def on_confirm(self) -> WrappedHandlerT: """The handler to invoke when the user accepts the value of the widget, usually by pressing return/enter on the keyboard. """ return self._on_confirm @on_confirm.setter - def on_confirm(self, handler): + def on_confirm(self, handler: OnConfirmHandlerT) -> None: self._on_confirm = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index f1beee1041..a9f3d16959 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -2,21 +2,44 @@ import datetime import warnings +from typing import Any, Protocol, Union -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +class OnChangeHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the time input is changed.""" + + +class OnChangeHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnChangeHandlerSync`.""" + + +class OnChangeHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnChangeHandlerSync`.""" + + +OnChangeHandlerT: TypeAlias = Union[ + OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator +] + + class TimeInput(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, value: datetime.time | None = None, min: datetime.time | None = None, max: datetime.time | None = None, - on_change: callable | None = None, + on_change: OnChangeHandlerT | None = None, ): """Create a new TimeInput widget. @@ -34,12 +57,12 @@ def __init__( # 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 + self.on_change = None # type: ignore[assignment] + self.min = min # type: ignore[assignment] + self.max = max # type: ignore[assignment] - self.value = value - self.on_change = on_change + self.value = value # type: ignore[assignment] + self.on_change = on_change # type: ignore[assignment] @property def value(self) -> datetime.time: @@ -51,7 +74,18 @@ def value(self) -> datetime.time: """ return self._impl.get_value() - def _convert_time(self, value): + @value.setter + def value(self, value: object) -> None: + value = self._convert_time(value) + + if value < self.min: + value = self.min + elif value > self.max: + value = self.max + + self._impl.set_value(value) + + def _convert_time(self, value: object) -> datetime.time: if value is None: value = datetime.datetime.now().time() elif isinstance(value, datetime.datetime): @@ -65,17 +99,6 @@ def _convert_time(self, value): return value.replace(microsecond=0) - @value.setter - def value(self, value): - value = self._convert_time(value) - - if value < self.min: - value = self.min - elif value > self.max: - value = self.max - - self._impl.set_value(value) - @property def min(self) -> datetime.time: """The minimum allowable time (inclusive). A value of ``None`` will be converted @@ -87,7 +110,7 @@ def min(self) -> datetime.time: return self._impl.get_min_time() @min.setter - def min(self, value): + def min(self, value: object) -> None: if value is None: min = datetime.time(0, 0, 0) else: @@ -110,7 +133,7 @@ def max(self) -> datetime.time: return self._impl.get_max_time() @max.setter - def max(self, value): + def max(self, value: object) -> None: if value is None: max = datetime.time(23, 59, 59) else: @@ -123,18 +146,18 @@ def max(self, value): self.value = max @property - def on_change(self) -> callable: + def on_change(self) -> WrappedHandlerT: """The handler to invoke when the time value changes.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnChangeHandlerT) -> None: self._on_change = wrapped_handler(self, handler) # 2023-05: Backwards compatibility class TimePicker(TimeInput): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): warnings.warn("TimePicker has been renamed TimeInput", DeprecationWarning) for old_name, new_name in [ @@ -155,29 +178,29 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @property - def min_time(self): + def min_time(self) -> datetime.time: warnings.warn( "TimePicker.min_time has been renamed TimeInput.min", DeprecationWarning ) return self.min @min_time.setter - def min_time(self, value): + def min_time(self, value: object) -> None: warnings.warn( "TimePicker.min_time has been renamed TimeInput.min", DeprecationWarning ) - self.min = value + self.min = value # type: ignore[assignment] @property - def max_time(self): + def max_time(self) -> datetime.time: warnings.warn( "TimePicker.max_time has been renamed TimeInput.max", DeprecationWarning ) return self.max @max_time.setter - def max_time(self, value): + def max_time(self, value: object) -> None: warnings.warn( "TimePicker.max_time has been renamed TimeInput.max", DeprecationWarning ) - self.max = value + self.max = value # type: ignore[assignment] diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 55608225b6..60ab6e0078 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -1,28 +1,74 @@ from __future__ import annotations import warnings -from typing import Any +from typing import Collection, Generic, Iterable, Literal, Protocol, TypeVar, Union -from toga.handlers import wrapped_handler +from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import Node, Source, TreeSource from toga.sources.accessors import build_accessors, to_accessor +from toga.sources.tree_source import TreeSourceDataT +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget +T = TypeVar("T") +SourceT = TypeVar("SourceT", bound=Source) -class Tree(Widget): + +class OnSelectHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the tree is selected.""" + + +class OnSelectHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnSelectHandlerSync`.""" + + +class OnSelectHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnSelectHandlerSync`.""" + + +OnSelectHandlerT: TypeAlias = Union[ + OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator +] + + +class OnActivateHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the tree is activated.""" + + +class OnActivateHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnActivateHandlerSync`.""" + + +class OnActivateHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnActivateHandlerSync`.""" + + +OnActivateHandlerT: TypeAlias = Union[ + OnActivateHandlerSync, OnActivateHandlerAsync, OnActivateHandlerGenerator +] + + +class Tree(Widget, Generic[T]): def __init__( self, - headings: list[str] | None = None, - id=None, - style=None, - data: Any = None, - accessors: list[str] | None = None, + headings: Iterable[str] | None = None, + id: str | None = None, + style: Pack | None = None, + data: SourceT | TreeSourceDataT[T] | None = None, + accessors: Collection[str] | None = None, multiple_select: bool = False, - on_select: callable | None = None, - on_activate: callable | None = None, + on_select: OnSelectHandlerT | None = None, + on_activate: OnActivateHandlerT | None = None, missing_value: str = "", - on_double_click=None, # DEPRECATED + on_double_click: None = None, # DEPRECATED ): """Create a new Tree widget. @@ -61,7 +107,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if on_double_click: - if on_activate: + if on_activate: # type: ignore[unreachable] raise ValueError("Cannot specify both on_double_click and on_activate") else: warnings.warn( @@ -73,12 +119,15 @@ def __init__( # End backwards compatibility. ###################################################################### + self._headings: list[str] | None + self._data: SourceT | TreeSource[T] + if headings is not None: self._headings = [heading.split("\n")[0] for heading in headings] self._accessors = build_accessors(self._headings, accessors) elif accessors is not None: self._headings = None - self._accessors = accessors + self._accessors = list(accessors) else: raise ValueError( "Cannot create a tree without either headings or accessors" @@ -87,18 +136,18 @@ def __init__( self._missing_value = missing_value or "" # Prime some properties that need to exist before the tree is created. - self.on_select = None - self.on_activate = None - self._data = None + self.on_select = None # type: ignore[assignment] + self.on_activate = None # type: ignore[assignment] + self._data = None # type: ignore[assignment] self._impl = self.factory.Tree(interface=self) - self.data = data + self.data = data # type: ignore[assignment] - self.on_select = on_select - self.on_activate = on_activate + self.on_select = on_select # type: ignore[assignment] + self.on_activate = on_activate # type: ignore[assignment] - @property - def enabled(self) -> bool: + @property # type: ignore[override] + def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? Tree widgets cannot be disabled; this property will always return True; any attempt to modify it will be ignored. @@ -106,15 +155,15 @@ def enabled(self) -> bool: return True @enabled.setter - def enabled(self, value): + def enabled(self, value: object) -> None: pass - def focus(self): - "No-op; Tree cannot accept input focus" + def focus(self) -> None: + """No-op; Tree cannot accept input focus.""" pass @property - def data(self) -> TreeSource: + def data(self) -> TreeSource[T]: """The data to display in the tree. When setting this property: @@ -127,14 +176,14 @@ def data(self) -> TreeSource: * Otherwise, the value must be a dictionary or an iterable, which is copied into a new TreeSource as shown :ref:`here `. """ - return self._data + return self._data # type: ignore[return-value] @data.setter - def data(self, data: Any): + def data(self, data: SourceT | TreeSourceDataT[T] | None) -> None: if data is None: self._data = TreeSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): - self._data = data + self._data = data # type: ignore[assignment] else: self._data = TreeSource(accessors=self._accessors, data=data) @@ -147,7 +196,7 @@ def multiple_select(self) -> bool: return self._multiple_select @property - def selection(self) -> list[Node] | Node | None: + def selection(self) -> list[Node[T]] | Node[T] | None: """The current selection of the tree. If multiple selection is enabled, returns a list of Node objects from the data @@ -159,7 +208,7 @@ def selection(self) -> list[Node] | Node | None: """ return self._impl.get_selection() - def expand(self, node: Node | None = None): + def expand(self, node: Node[T] | None = None) -> None: """Expand the specified node of the tree. If no node is provided, all nodes of the tree will be expanded. @@ -176,7 +225,7 @@ def expand(self, node: Node | None = None): else: self._impl.expand_node(node) - def collapse(self, node: Node | None = None): + def collapse(self, node: Node[T] | None = None) -> None: """Collapse the specified node of the tree. If no node is provided, all nodes of the tree will be collapsed. @@ -191,7 +240,7 @@ def collapse(self, node: Node | None = None): else: self._impl.collapse_node(node) - def append_column(self, heading: str, accessor: str | None = None): + def append_column(self, heading: str, accessor: str | None = None) -> None: """Append a column to the end of the tree. :param heading: The heading for the new column. @@ -204,9 +253,9 @@ def append_column(self, heading: str, accessor: str | None = None): def insert_column( self, index: int | str, - heading: str | None, + heading: str, accessor: str | None = None, - ): + ) -> None: """Insert an additional column into the tree. :param index: The index at which to insert the column, or the accessor of the @@ -220,7 +269,7 @@ def insert_column( if self._headings is None: if accessor is None: raise ValueError("Must specify an accessor on a tree without headings") - heading = None + heading = None # type: ignore[assignment] elif not accessor: accessor = to_accessor(heading) @@ -239,7 +288,7 @@ def insert_column( self._impl.insert_column(index, heading, accessor) - def remove_column(self, column: int | str): + def remove_column(self, column: int | str) -> None: """Remove a tree column. :param column: The index of the column to remove, or the accessor of the column @@ -261,7 +310,7 @@ def remove_column(self, column: int | str): self._impl.remove_column(index) @property - def headings(self) -> list[str]: + def headings(self) -> list[str] | None: """The column headings for the tree (read-only)""" return self._headings @@ -278,22 +327,22 @@ def missing_value(self) -> str: return self._missing_value @property - def on_select(self) -> callable: + def on_select(self) -> WrappedHandlerT: """The callback function that is invoked when a row of the tree is selected.""" return self._on_select @on_select.setter - def on_select(self, handler: callable): + def on_select(self, handler: OnSelectHandlerT) -> None: self._on_select = wrapped_handler(self, handler) @property - def on_activate(self) -> callable: + def on_activate(self) -> WrappedHandlerT: """The callback function that is invoked when a row of the tree is activated, usually with a double click or similar action.""" return self._on_activate @on_activate.setter - def on_activate(self, handler): + def on_activate(self, handler: OnActivateHandlerT) -> None: self._on_activate = wrapped_handler(self, handler) ###################################################################### @@ -301,7 +350,7 @@ def on_activate(self, handler): ###################################################################### @property - def on_double_click(self): + def on_double_click(self) -> WrappedHandlerT: """**DEPRECATED**: Use ``on_activate``""" warnings.warn( "Tree.on_double_click has been renamed Tree.on_activate.", @@ -310,7 +359,7 @@ def on_double_click(self): return self.on_activate @on_double_click.setter - def on_double_click(self, handler): + def on_double_click(self, handler: OnActivateHandlerT) -> None: warnings.warn( "Tree.on_double_click has been renamed Tree.on_activate.", DeprecationWarning, diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index f2282efe73..97a98e463b 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -1,8 +1,16 @@ from __future__ import annotations import asyncio +from typing import Callable, Protocol, Union -from toga.handlers import AsyncResult, wrapped_handler +from toga.handlers import ( + AsyncResult, + HandlerGeneratorReturnT, + WrappedHandlerT, + wrapped_handler, +) +from toga.style import Pack +from toga.types import TypeAlias from .base import Widget @@ -11,14 +19,34 @@ class JavaScriptResult(AsyncResult): RESULT_TYPE = "JavaScript" +class OnWebViewLoadHandlerSync(Protocol): + def __call__(self, /) -> object: + """A handler to invoke when the WebView is loaded.""" + + +class OnWebViewLoadHandlerAsync(Protocol): + async def __call__(self, /) -> object: + """Async definition of :any:`OnWebViewLoadHandlerSync`.""" + + +class OnWebViewLoadHandlerGenerator(Protocol): + def __call__(self, /) -> HandlerGeneratorReturnT[object]: + """Generator definition of :any:`OnWebViewLoadHandlerSync`.""" + + +OnWebViewLoadHandlerT: TypeAlias = Union[ + OnWebViewLoadHandlerSync, OnWebViewLoadHandlerAsync, OnWebViewLoadHandlerGenerator +] + + class WebView(Widget): def __init__( self, - id=None, - style=None, + id: str | None = None, + style: Pack | None = None, url: str | None = None, user_agent: str | None = None, - on_webview_load: callable | None = None, + on_webview_load: OnWebViewLoadHandlerT | None = None, ): """Create a new WebView widget. @@ -32,21 +60,18 @@ def __init__( :param on_webview_load: A handler that will be invoked when the web view finishes loading. """ - super().__init__(id=id, style=style) self._impl = self.factory.WebView(interface=self) - self.user_agent = user_agent + self.user_agent = user_agent # type: ignore[assignment] # Set the load handler before loading the first URL. - self.on_webview_load = on_webview_load + self.on_webview_load = on_webview_load # type: ignore[assignment] self.url = url - def _set_url(self, url, future): + def _set_url(self, url: str | None, future: asyncio.Future | None) -> None: # type: ignore[type-arg] # Utility method for validating and setting the URL with a future. - if (url is not None) and not ( - url.startswith("https://") or url.startswith("http://") - ): + if (url is not None) and not url.startswith(("https://", "http://")): raise ValueError("WebView can only display http:// and https:// URLs") self._impl.set_url(url, future=future) @@ -62,10 +87,10 @@ def url(self) -> str | None: return self._impl.get_url() @url.setter - def url(self, value): + def url(self, value: str | None) -> None: self._set_url(value, future=None) - async def load_url(self, url: str): + async def load_url(self, url: str) -> asyncio.Future: # type: ignore[type-arg] """Load a URL, and wait until the next :any:`on_webview_load` event. **Note:** On Android, this method will return immediately. @@ -78,7 +103,7 @@ async def load_url(self, url: str): return await loaded_future @property - def on_webview_load(self) -> callable: + def on_webview_load(self) -> WrappedHandlerT: """The handler to invoke when the web view finishes loading. Rendering web content is a complex, multi-threaded process. Although a page @@ -95,7 +120,7 @@ def on_webview_load(self) -> callable: return self._on_webview_load @on_webview_load.setter - def on_webview_load(self, handler): + def on_webview_load(self, handler: OnWebViewLoadHandlerT) -> None: if handler and not getattr(self._impl, "SUPPORTS_ON_WEBVIEW_LOAD", True): self.factory.not_implemented("WebView.on_webview_load") @@ -111,10 +136,10 @@ def user_agent(self) -> str: return self._impl.get_user_agent() @user_agent.setter - def user_agent(self, value): + def user_agent(self, value: str) -> None: self._impl.set_user_agent(value) - def set_content(self, root_url: str, content: str): + def set_content(self, root_url: str, content: str) -> None: """Set the HTML content of the WebView. **Note:** On Android and Windows, the ``root_url`` argument is ignored. Calling @@ -126,7 +151,11 @@ def set_content(self, root_url: str, content: str): """ self._impl.set_content(root_url, content) - def evaluate_javascript(self, javascript, on_result=None) -> JavaScriptResult: + def evaluate_javascript( + self, + javascript: str, + on_result: Callable[[], object] | None = None, + ) -> JavaScriptResult: """Evaluate a JavaScript expression. **This is an asynchronous method**. There is no guarantee that the JavaScript diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 2fdb0b8593..70cbf4f5d9 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -2,25 +2,28 @@ import warnings from builtins import id as identifier -from collections.abc import Mapping, MutableSet from pathlib import Path from typing import ( TYPE_CHECKING, Any, - ItemsView, Iterator, - KeysView, Literal, Protocol, TypeVar, - ValuesView, + Union, overload, ) -from toga.command import Command, CommandSet -from toga.handlers import AsyncResult, wrapped_handler +from toga.command import CommandSet +from toga.handlers import ( + AsyncResult, + HandlerGeneratorReturnT, + WrappedHandlerT, + wrapped_handler, +) from toga.images import Image from toga.platform import get_platform_factory +from toga.types import TypeAlias if TYPE_CHECKING: from toga.app import App @@ -33,7 +36,7 @@ class FilteredWidgetRegistry: # A class that exposes a mapping lookup interface, filtered to widgets from a single # window. The underlying data store is on the app. - def __init__(self, window): + def __init__(self, window: Window) -> None: self._window = window def __len__(self) -> int: @@ -58,58 +61,86 @@ def __iter__(self) -> Iterator[Widget]: def __repr__(self) -> str: return "{" + ", ".join(f"{k!r}: {v!r}" for k, v in sorted(self.items())) + "}" - def items(self) -> ItemsView: + def items(self) -> Iterator[tuple[str, Widget]]: for item in self._window.app.widgets.items(): if item[1].window == self._window: yield item - def keys(self) -> KeysView: + def keys(self) -> Iterator[str]: for item in self._window.app.widgets.items(): if item[1].window == self._window: yield item[0] - def values(self) -> ValuesView: + def values(self) -> Iterator[Widget]: for item in self._window.app.widgets.items(): if item[1].window == self._window: yield item[1] -class OnCloseHandler(Protocol): - def __call__(self, window: Window, **kwargs: Any) -> bool: +class OnCloseHandlerSync(Protocol): + def __call__(self, window: Window, /) -> bool: """A handler to invoke when a window is about to close. The return value of this callback controls whether the window is allowed to close. This can be used to prevent a window closing with unsaved changes, etc. :param window: The window instance that is closing. - :param kwargs: Ensures compatibility with additional arguments introduced in - future versions. - :returns: ``True`` if the window is allowed to close; ``False`` if the window is not - allowed to close. + :returns: ``True`` if the window is allowed to close; ``False`` if the window + is not allowed to close. """ - ... -T = TypeVar("T") +class OnCloseHandlerAsync(Protocol): + async def __call__(self, window: Window, /) -> bool: + """Async definition of :any:`OnCloseHandlerSync`.""" + + +class OnCloseHandlerGenerator(Protocol): + def __call__(self, window: Window, /) -> HandlerGeneratorReturnT[bool]: + """Generator definition of :any:`OnCloseHandlerSync`.""" + +OnCloseHandlerT: TypeAlias = Union[ + OnCloseHandlerSync, OnCloseHandlerAsync, OnCloseHandlerGenerator +] -class DialogResultHandler(Protocol[T]): - def __call__(self, window: Window, result: T, **kwargs: Any) -> None: +_DialogResultT = TypeVar("_DialogResultT", contravariant=True) + + +class DialogResultHandlerSync(Protocol[_DialogResultT]): + def __call__(self, window: Window, result: _DialogResultT, /) -> Any: """A handler to invoke when a dialog is closed. :param window: The window that opened the dialog. :param result: The result returned by the dialog. - :param kwargs: Ensures compatibility with additional arguments introduced in - future versions. """ - ... + + +class DialogResultHandlerAsync(Protocol[_DialogResultT]): + async def __call__(self, window: Window, result: _DialogResultT, /) -> Any: + """Async definition of :any:`DialogResultHandlerSync`.""" + + +class DialogResultHandlerGenerator(Protocol[_DialogResultT]): + def __call__( + self, window: Window, result: _DialogResultT, / + ) -> HandlerGeneratorReturnT[Any]: + """Generator definition of :any:`DialogResultHandlerSync`.""" + + +DialogResultHandlerT: TypeAlias = Union[ + DialogResultHandlerSync[_DialogResultT], + DialogResultHandlerAsync[_DialogResultT], + DialogResultHandlerGenerator[_DialogResultT], +] class Dialog(AsyncResult): RESULT_TYPE = "dialog" - def __init__(self, window: Window, on_result: DialogResultHandler[Any]): - super().__init__(on_result=on_result) + def __init__(self, window: Window, on_result: DialogResultHandlerT[Any]): + # TODO:PR: should DialogResultHandlerT include the "exception" arg... + super().__init__(on_result=on_result) # type:ignore[arg-type] self.window = window self.app = window.app @@ -126,10 +157,10 @@ def __init__( resizable: bool = True, closable: bool = True, minimizable: bool = True, - on_close: OnCloseHandler | None = None, + on_close: OnCloseHandlerT | None = None, content: Widget | None = None, - resizeable=None, # DEPRECATED - closeable=None, # DEPRECATED + resizeable: None = None, # DEPRECATED + closeable: None = None, # DEPRECATED ) -> None: """Create a new Window. @@ -152,14 +183,14 @@ def __init__( # 2023-08: Backwards compatibility ###################################################################### if resizeable is not None: - warnings.warn( + warnings.warn( # type: ignore[unreachable] "Window.resizeable has been renamed Window.resizable", DeprecationWarning, ) resizable = resizeable if closeable is not None: - warnings.warn( + warnings.warn( # type: ignore[unreachable] "Window.closeable has been renamed Window.closable", DeprecationWarning, ) @@ -172,8 +203,8 @@ def __init__( from toga import App self._id = str(id if id else identifier(self)) - self._impl = None - self._content = None + self._impl: Any = None + self._content: Widget | None = None self._is_full_screen = False self._closed = False @@ -190,7 +221,8 @@ def __init__( ) # Add the window to the app - self._app = None + # _app will only be None until the window is added to the app below + self._app: App = None # type: ignore[assignment] if App.app is None: raise RuntimeError("Cannot create a Window before creating an App") App.app.windows.add(self) @@ -204,7 +236,7 @@ def __init__( self.on_close = on_close - def __lt__(self, other) -> bool: + def __lt__(self, other: Window) -> bool: return self.id < other.id ###################################################################### @@ -326,12 +358,12 @@ def content(self, widget: Widget) -> None: widget.refresh() @property - def toolbar(self) -> MutableSet[Command]: + def toolbar(self) -> CommandSet: """Toolbar for the window.""" return self._toolbar @property - def widgets(self) -> Mapping[str, Widget]: + def widgets(self) -> FilteredWidgetRegistry: """The widgets contained in the window. Can be used to look up widgets by ID (e.g., ``window.widgets["my_id"]``). @@ -461,7 +493,9 @@ def full_screen(self, is_full_screen: bool) -> None: # Window capabilities ###################################################################### - def as_image(self, format: type[ImageT] = Image) -> ImageT: + def as_image( + self, format: type[ImageT] = Image # type:ignore[assignment] + ) -> ImageT: """Render the current contents of the window as an image. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also @@ -477,12 +511,12 @@ def as_image(self, format: type[ImageT] = Image) -> ImageT: ###################################################################### @property - def on_close(self) -> OnCloseHandler: + def on_close(self) -> WrappedHandlerT | None: """The handler to invoke if the user attempts to close the window.""" return self._on_close @on_close.setter - def on_close(self, handler: OnCloseHandler | None) -> None: + def on_close(self, handler: OnCloseHandlerT | None) -> None: def cleanup(window: Window, should_close: bool) -> None: if should_close or handler is None: window.close() @@ -497,7 +531,7 @@ def info_dialog( self, title: str, message: str, - on_result: DialogResultHandler[None] | None = None, + on_result: DialogResultHandlerT[None] | None = None, ) -> Dialog: """Ask the user to acknowledge some information. @@ -515,7 +549,11 @@ def info_dialog( """ dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) self.factory.dialogs.InfoDialog(dialog, title, message) return dialog @@ -524,7 +562,7 @@ def question_dialog( self, title: str, message: str, - on_result: DialogResultHandler[bool] | None = None, + on_result: DialogResultHandlerT[bool] | None = None, ) -> Dialog: """Ask the user a yes/no question. @@ -542,7 +580,11 @@ def question_dialog( """ dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) self.factory.dialogs.QuestionDialog(dialog, title, message) return dialog @@ -551,7 +593,7 @@ def confirm_dialog( self, title: str, message: str, - on_result: DialogResultHandler[bool] | None = None, + on_result: DialogResultHandlerT[bool] | None = None, ) -> Dialog: """Ask the user to confirm if they wish to proceed with an action. @@ -570,7 +612,11 @@ def confirm_dialog( """ dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) self.factory.dialogs.ConfirmDialog(dialog, title, message) return dialog @@ -579,7 +625,7 @@ def error_dialog( self, title: str, message: str, - on_result: DialogResultHandler[None] | None = None, + on_result: DialogResultHandlerT[None] | None = None, ) -> Dialog: """Ask the user to acknowledge an error state. @@ -597,7 +643,11 @@ def error_dialog( """ dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) self.factory.dialogs.ErrorDialog(dialog, title, message) return dialog @@ -609,7 +659,7 @@ def stack_trace_dialog( message: str, content: str, retry: Literal[False] = False, - on_result: DialogResultHandler[None] | None = None, + on_result: DialogResultHandlerT[None] | None = None, ) -> Dialog: ... @overload @@ -618,8 +668,8 @@ def stack_trace_dialog( title: str, message: str, content: str, - retry: Literal[True] = False, - on_result: DialogResultHandler[bool] | None = None, + retry: Literal[True] = True, + on_result: DialogResultHandlerT[bool] | None = None, ) -> Dialog: ... @overload @@ -629,7 +679,9 @@ def stack_trace_dialog( message: str, content: str, retry: bool = False, - on_result: DialogResultHandler[bool | None] | None = None, + on_result: ( + DialogResultHandlerT[bool] | DialogResultHandlerT[None] | None + ) = None, ) -> Dialog: ... def stack_trace_dialog( @@ -638,7 +690,9 @@ def stack_trace_dialog( message: str, content: str, retry: bool = False, - on_result: DialogResultHandler[bool | None] | None = None, + on_result: ( + DialogResultHandlerT[bool] | DialogResultHandlerT[None] | None + ) = None, ) -> Dialog: """Open a dialog to display a large block of text, such as a stack trace. @@ -658,7 +712,11 @@ def stack_trace_dialog( """ dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) self.factory.dialogs.StackTraceDialog( dialog, @@ -674,7 +732,7 @@ def save_file_dialog( title: str, suggested_filename: Path | str, file_types: list[str] | None = None, - on_result: DialogResultHandler[Path | None] | None = None, + on_result: DialogResultHandlerT[Path | None] | None = None, ) -> Dialog: """Prompt the user for a location to save a file. @@ -695,12 +753,16 @@ def save_file_dialog( """ dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) # Convert suggested filename to a path (if it isn't already), # and break it into a filename and a directory suggested_path = Path(suggested_filename) - initial_directory = suggested_path.parent + initial_directory: Path | None = suggested_path.parent if initial_directory == Path("."): initial_directory = None filename = suggested_path.name @@ -721,8 +783,10 @@ def open_file_dialog( initial_directory: Path | str | None = None, file_types: list[str] | None = None, multiple_select: Literal[False] = False, - on_result: DialogResultHandler[Path | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[Path] | DialogResultHandlerT[None] | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: ... @overload @@ -732,8 +796,10 @@ def open_file_dialog( initial_directory: Path | str | None = None, file_types: list[str] | None = None, multiple_select: Literal[True] = True, - on_result: DialogResultHandler[list[Path] | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[list[Path]] | DialogResultHandlerT[None] | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: ... @overload @@ -743,8 +809,13 @@ def open_file_dialog( initial_directory: Path | str | None = None, file_types: list[str] | None = None, multiple_select: bool = False, - on_result: DialogResultHandler[list[Path] | Path | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[list[Path]] + | DialogResultHandlerT[Path] + | DialogResultHandlerT[None] + | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: ... def open_file_dialog( @@ -753,8 +824,13 @@ def open_file_dialog( initial_directory: Path | str | None = None, file_types: list[str] | None = None, multiple_select: bool = False, - on_result: DialogResultHandler[list[Path] | Path | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[list[Path]] + | DialogResultHandlerT[Path] + | DialogResultHandlerT[None] + | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: """Prompt the user to select a file (or files) to open. @@ -782,7 +858,7 @@ def open_file_dialog( # 2023-08: Backwards compatibility ###################################################################### if multiselect is not None: - warnings.warn( + warnings.warn( # type: ignore[unreachable] "open_file_dialog(multiselect) has been renamed multiple_select", DeprecationWarning, ) @@ -793,7 +869,11 @@ def open_file_dialog( dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) self.factory.dialogs.OpenFileDialog( dialog, @@ -810,8 +890,10 @@ def select_folder_dialog( title: str, initial_directory: Path | str | None = None, multiple_select: Literal[False] = False, - on_result: DialogResultHandler[Path | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[Path] | DialogResultHandlerT[None] | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: ... @overload @@ -820,8 +902,10 @@ def select_folder_dialog( title: str, initial_directory: Path | str | None = None, multiple_select: Literal[True] = True, - on_result: DialogResultHandler[list[Path] | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[list[Path]] | DialogResultHandlerT[None] | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: ... @overload @@ -830,8 +914,13 @@ def select_folder_dialog( title: str, initial_directory: Path | str | None = None, multiple_select: bool = False, - on_result: DialogResultHandler[list[Path] | Path | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[list[Path]] + | DialogResultHandlerT[Path] + | DialogResultHandlerT[None] + | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: ... def select_folder_dialog( @@ -839,8 +928,13 @@ def select_folder_dialog( title: str, initial_directory: Path | str | None = None, multiple_select: bool = False, - on_result: DialogResultHandler[list[Path] | Path | None] | None = None, - multiselect=None, # DEPRECATED + on_result: ( + DialogResultHandlerT[list[Path]] + | DialogResultHandlerT[Path] + | DialogResultHandlerT[None] + | None + ) = None, + multiselect: None = None, # DEPRECATED ) -> Dialog: """Prompt the user to select a directory (or directories). @@ -867,7 +961,7 @@ def select_folder_dialog( # 2023-08: Backwards compatibility ###################################################################### if multiselect is not None: - warnings.warn( + warnings.warn( # type: ignore[unreachable] "select_folder_dialog(multiselect) has been renamed multiple_select", DeprecationWarning, ) @@ -878,7 +972,11 @@ def select_folder_dialog( dialog = Dialog( self, - on_result=wrapped_handler(self, on_result) if on_result else None, + on_result=( + wrapped_handler(self, on_result) + if on_result + else None # type:ignore[arg-type] # TODO:PR:revisit + ), ) self.factory.dialogs.SelectFolderDialog( dialog, @@ -891,7 +989,6 @@ def select_folder_dialog( ###################################################################### # 2023-08: Backwards compatibility ###################################################################### - @property def resizeable(self) -> bool: """**DEPRECATED** Use :attr:`resizable`""" @@ -909,3 +1006,7 @@ def closeable(self) -> bool: DeprecationWarning, ) return self._closable + + ###################################################################### + # End Backwards compatibility + ###################################################################### diff --git a/docs/reference/api/app.rst b/docs/reference/api/app.rst index 44505248dc..28330c3069 100644 --- a/docs/reference/api/app.rst +++ b/docs/reference/api/app.rst @@ -86,5 +86,9 @@ Reference .. autoclass:: toga.App .. autoprotocol:: toga.app.AppStartupMethod -.. autoprotocol:: toga.app.BackgroundTask -.. autoprotocol:: toga.app.OnExitHandler +.. autoprotocol:: toga.app.BackgroundTaskSync +.. autoprotocol:: toga.app.BackgroundTaskAsync +.. autoprotocol:: toga.app.BackgroundTaskGenerator +.. autoprotocol:: toga.app.OnExitHandlerSync +.. autoprotocol:: toga.app.OnExitHandlerAsync +.. autoprotocol:: toga.app.OnExitHandlerGenerator diff --git a/docs/reference/api/containers/optioncontainer.rst b/docs/reference/api/containers/optioncontainer.rst index 284d584c69..4bd1bbdb1b 100644 --- a/docs/reference/api/containers/optioncontainer.rst +++ b/docs/reference/api/containers/optioncontainer.rst @@ -70,7 +70,7 @@ of content can be modified after initial construction: icecream = toga.Box() container.content.append("Ice Cream", icecream, toga.Icon("icecream")) -OptionContainer content can also be specified by using :any:`OptionItem` instances +OptionContainer content can also be specified by using :any:`toga.OptionItem` instances instead of tuples. This enables you to be explicit when setting an icon or enabled status; it also allows you to set the initial enabled status *without* setting an icon: @@ -119,7 +119,7 @@ item, you can specify an item using: # Delete tab labeled "Pasta" del container.content["Pasta"] -* A reference to an :any:`OptionItem`: +* A reference to an :any:`toga.OptionItem`: .. code-block:: python @@ -183,7 +183,7 @@ Reference for the tab; * a 4-tuple, containing the title, content widget, :any:`icon ` for the tab, and enabled status; or - * an :any:`OptionItem` instance. + * an :any:`toga.OptionItem` instance. .. autoclass:: toga.OptionContainer :exclude-members: app, window @@ -193,4 +193,8 @@ Reference .. autoclass:: toga.widgets.optioncontainer.OptionList :special-members: __getitem__, __delitem__ -.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandler +.. autoclass:: toga.widgets.optioncontainer.OptionItem + +.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandlerSync +.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandlerAsync +.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandlerGenerator diff --git a/docs/reference/api/containers/scrollcontainer.rst b/docs/reference/api/containers/scrollcontainer.rst index cb068354f1..368c3bbbb9 100644 --- a/docs/reference/api/containers/scrollcontainer.rst +++ b/docs/reference/api/containers/scrollcontainer.rst @@ -60,3 +60,7 @@ Reference .. autoclass:: toga.ScrollContainer :exclude-members: window, app + +.. autoprotocol:: toga.widgets.scrollcontainer.OnScrollHandlerSync +.. autoprotocol:: toga.widgets.scrollcontainer.OnScrollHandlerAsync +.. autoprotocol:: toga.widgets.scrollcontainer.OnScrollHandlerGenerator diff --git a/docs/reference/api/resources/command.rst b/docs/reference/api/resources/command.rst index 9f2067bfcb..cb27a0851d 100644 --- a/docs/reference/api/resources/command.rst +++ b/docs/reference/api/resources/command.rst @@ -83,4 +83,6 @@ Reference .. autoclass:: toga.Group :exclude-members: key -.. autoprotocol:: toga.command.ActionHandler +.. autoprotocol:: toga.command.ActionHandlerSync +.. autoprotocol:: toga.command.ActionHandlerAsync +.. autoprotocol:: toga.command.ActionHandlerGenerator diff --git a/docs/reference/api/widgets/button.rst b/docs/reference/api/widgets/button.rst index ba1aab9694..890ee25648 100644 --- a/docs/reference/api/widgets/button.rst +++ b/docs/reference/api/widgets/button.rst @@ -85,4 +85,6 @@ Reference .. autoclass:: toga.Button -.. autoprotocol:: toga.widgets.button.OnPressHandler +.. autoprotocol:: toga.widgets.button.OnPressHandlerSync +.. autoprotocol:: toga.widgets.button.OnPressHandlerAsync +.. autoprotocol:: toga.widgets.button.OnPressHandlerGenerator diff --git a/docs/reference/api/widgets/dateinput.rst b/docs/reference/api/widgets/dateinput.rst index 2854869094..2e40c276f0 100644 --- a/docs/reference/api/widgets/dateinput.rst +++ b/docs/reference/api/widgets/dateinput.rst @@ -60,3 +60,7 @@ Reference --------- .. autoclass:: toga.DateInput + +.. autoprotocol:: toga.widgets.dateinput.OnChangeHandlerSync +.. autoprotocol:: toga.widgets.dateinput.OnChangeHandlerAsync +.. autoprotocol:: toga.widgets.dateinput.OnChangeHandlerGenerator diff --git a/docs/reference/api/widgets/detailedlist.rst b/docs/reference/api/widgets/detailedlist.rst index 55693e2bb2..f32f6a7587 100644 --- a/docs/reference/api/widgets/detailedlist.rst +++ b/docs/reference/api/widgets/detailedlist.rst @@ -146,3 +146,16 @@ Reference --------- .. autoclass:: toga.DetailedList + +.. autoprotocol:: toga.widgets.detailedlist.OnPrimaryActionHandlerSync +.. autoprotocol:: toga.widgets.detailedlist.OnPrimaryActionHandlerAsync +.. autoprotocol:: toga.widgets.detailedlist.OnPrimaryActionHandlerGenerator +.. autoprotocol:: toga.widgets.detailedlist.OnSecondaryActionHandlerSync +.. autoprotocol:: toga.widgets.detailedlist.OnSecondaryActionHandlerAsync +.. autoprotocol:: toga.widgets.detailedlist.OnSecondaryActionHandlerGenerator +.. autoprotocol:: toga.widgets.detailedlist.OnRefreshHandlerSync +.. autoprotocol:: toga.widgets.detailedlist.OnRefreshHandlerAsync +.. autoprotocol:: toga.widgets.detailedlist.OnRefreshHandlerGenerator +.. autoprotocol:: toga.widgets.detailedlist.OnSelectHandlerSync +.. autoprotocol:: toga.widgets.detailedlist.OnSelectHandlerAsync +.. autoprotocol:: toga.widgets.detailedlist.OnSelectHandlerGenerator diff --git a/docs/reference/api/widgets/mapview.rst b/docs/reference/api/widgets/mapview.rst index ad383e1c57..2098acd019 100644 --- a/docs/reference/api/widgets/mapview.rst +++ b/docs/reference/api/widgets/mapview.rst @@ -145,4 +145,7 @@ Reference .. autoclass:: toga.widgets.mapview.MapPinSet -.. autoprotocol:: toga.widgets.mapview.OnSelectHandler +.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerSync +.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerAsync +.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerGenerator +.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerT diff --git a/docs/reference/api/widgets/multilinetextinput.rst b/docs/reference/api/widgets/multilinetextinput.rst index 35fbe4f176..bfc17d6bf0 100644 --- a/docs/reference/api/widgets/multilinetextinput.rst +++ b/docs/reference/api/widgets/multilinetextinput.rst @@ -71,3 +71,7 @@ Reference --------- .. autoclass:: toga.MultilineTextInput + +.. autoprotocol:: toga.widgets.multilinetextinput.OnChangeHandlerSync +.. autoprotocol:: toga.widgets.multilinetextinput.OnChangeHandlerAsync +.. autoprotocol:: toga.widgets.multilinetextinput.OnChangeHandlerGenerator diff --git a/docs/reference/api/widgets/numberinput.rst b/docs/reference/api/widgets/numberinput.rst index 68bde9474d..5129146bd3 100644 --- a/docs/reference/api/widgets/numberinput.rst +++ b/docs/reference/api/widgets/numberinput.rst @@ -61,3 +61,7 @@ Reference --------- .. autoclass:: toga.NumberInput + +.. autoprotocol:: toga.widgets.numberinput.OnChangeHandlerSync +.. autoprotocol:: toga.widgets.numberinput.OnChangeHandlerAsync +.. autoprotocol:: toga.widgets.numberinput.OnChangeHandlerGenerator diff --git a/docs/reference/api/widgets/slider.rst b/docs/reference/api/widgets/slider.rst index a2569d5bdb..a52fad5133 100644 --- a/docs/reference/api/widgets/slider.rst +++ b/docs/reference/api/widgets/slider.rst @@ -69,3 +69,13 @@ Reference --------- .. autoclass:: toga.Slider + +.. autoprotocol:: toga.widgets.slider.OnChangeHandlerSync +.. autoprotocol:: toga.widgets.slider.OnChangeHandlerAsync +.. autoprotocol:: toga.widgets.slider.OnChangeHandlerGenerator +.. autoprotocol:: toga.widgets.slider.OnPressHandlerSync +.. autoprotocol:: toga.widgets.slider.OnPressHandlerAsync +.. autoprotocol:: toga.widgets.slider.OnPressHandlerGenerator +.. autoprotocol:: toga.widgets.slider.OnReleaseHandlerSync +.. autoprotocol:: toga.widgets.slider.OnReleaseHandlerAsync +.. autoprotocol:: toga.widgets.slider.OnReleaseHandlerGenerator diff --git a/docs/reference/api/widgets/switch.rst b/docs/reference/api/widgets/switch.rst index d4081b9141..77662b6aa1 100644 --- a/docs/reference/api/widgets/switch.rst +++ b/docs/reference/api/widgets/switch.rst @@ -84,3 +84,7 @@ Reference --------- .. autoclass:: toga.Switch + +.. autoprotocol:: toga.widgets.switch.OnChangeHandlerSync +.. autoprotocol:: toga.widgets.switch.OnChangeHandlerAsync +.. autoprotocol:: toga.widgets.switch.OnChangeHandlerGenerator diff --git a/docs/reference/api/widgets/table.rst b/docs/reference/api/widgets/table.rst index 66f6c8dfbd..0df9acbe2d 100644 --- a/docs/reference/api/widgets/table.rst +++ b/docs/reference/api/widgets/table.rst @@ -135,3 +135,10 @@ Reference --------- .. autoclass:: toga.Table + +.. autoprotocol:: toga.widgets.table.OnSelectHandlerSync +.. autoprotocol:: toga.widgets.table.OnSelectHandlerAsync +.. autoprotocol:: toga.widgets.table.OnSelectHandlerGenerator +.. autoprotocol:: toga.widgets.table.OnActivateHandlerSync +.. autoprotocol:: toga.widgets.table.OnActivateHandlerAsync +.. autoprotocol:: toga.widgets.table.OnActivateHandlerGenerator diff --git a/docs/reference/api/widgets/textinput.rst b/docs/reference/api/widgets/textinput.rst index f95e64d242..436e86a9e2 100644 --- a/docs/reference/api/widgets/textinput.rst +++ b/docs/reference/api/widgets/textinput.rst @@ -94,3 +94,16 @@ Reference --------- .. autoclass:: toga.TextInput + +.. autoprotocol:: toga.widgets.textinput.OnChangeHandlerSync +.. autoprotocol:: toga.widgets.textinput.OnChangeHandlerAsync +.. autoprotocol:: toga.widgets.textinput.OnChangeHandlerGenerator +.. autoprotocol:: toga.widgets.textinput.OnConfirmHandlerSync +.. autoprotocol:: toga.widgets.textinput.OnConfirmHandlerAsync +.. autoprotocol:: toga.widgets.textinput.OnConfirmHandlerGenerator +.. autoprotocol:: toga.widgets.textinput.OnGainFocusHandlerSync +.. autoprotocol:: toga.widgets.textinput.OnGainFocusHandlerAsync +.. autoprotocol:: toga.widgets.textinput.OnGainFocusHandlerGenerator +.. autoprotocol:: toga.widgets.textinput.OnLoseFocusHandlerSync +.. autoprotocol:: toga.widgets.textinput.OnLoseFocusHandlerAsync +.. autoprotocol:: toga.widgets.textinput.OnLoseFocusHandlerGenerator diff --git a/docs/reference/api/widgets/timeinput.rst b/docs/reference/api/widgets/timeinput.rst index c6d8527b72..205eaabf55 100644 --- a/docs/reference/api/widgets/timeinput.rst +++ b/docs/reference/api/widgets/timeinput.rst @@ -64,3 +64,7 @@ Reference --------- .. autoclass:: toga.TimeInput + +.. autoprotocol:: toga.widgets.timeinput.OnChangeHandlerSync +.. autoprotocol:: toga.widgets.timeinput.OnChangeHandlerAsync +.. autoprotocol:: toga.widgets.timeinput.OnChangeHandlerGenerator diff --git a/docs/reference/api/widgets/tree.rst b/docs/reference/api/widgets/tree.rst index 9bb05f5706..4174c5909a 100644 --- a/docs/reference/api/widgets/tree.rst +++ b/docs/reference/api/widgets/tree.rst @@ -152,3 +152,10 @@ Reference --------- .. autoclass:: toga.Tree + +.. autoprotocol:: toga.widgets.tree.OnSelectHandlerSync +.. autoprotocol:: toga.widgets.tree.OnSelectHandlerAsync +.. autoprotocol:: toga.widgets.tree.OnSelectHandlerGenerator +.. autoprotocol:: toga.widgets.tree.OnActivateHandlerSync +.. autoprotocol:: toga.widgets.tree.OnActivateHandlerAsync +.. autoprotocol:: toga.widgets.tree.OnActivateHandlerGenerator diff --git a/docs/reference/api/widgets/webview.rst b/docs/reference/api/widgets/webview.rst index 0927cfc2f2..a0a495866a 100644 --- a/docs/reference/api/widgets/webview.rst +++ b/docs/reference/api/widgets/webview.rst @@ -109,3 +109,7 @@ Reference --------- .. autoclass:: toga.WebView + +.. autoprotocol:: toga.widgets.webview.OnWebViewLoadHandlerSync +.. autoprotocol:: toga.widgets.webview.OnWebViewLoadHandlerAsync +.. autoprotocol:: toga.widgets.webview.OnWebViewLoadHandlerGenerator diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 2f5cb2a368..c73803c063 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -42,14 +42,14 @@ An operating system-managed container of widgets. Usage ----- -A window is the top-level container that the operating system uses to display widgets. A -window may also have other decorations, such as a title bar or toolbar. +A window is the top-level container that the operating system uses to display widgets. +A window may also have other decorations, such as a title bar or toolbar. When first created, a window is not visible. To display it, call the :meth:`~toga.Window.show` method. The window has content, which will usually be a container widget of some kind. The -content of the window can be changed by re-assigning its `content` attribute to a +content of the window can be changed by re-assigning its ``content`` attribute to a different widget. .. code-block:: python @@ -96,5 +96,10 @@ Reference .. autoclass:: toga.Window -.. autoprotocol:: toga.window.OnCloseHandler -.. autoprotocol:: toga.window.DialogResultHandler +.. autoprotocol:: toga.window.Dialog +.. autoprotocol:: toga.window.OnCloseHandlerSync +.. autoprotocol:: toga.window.OnCloseHandlerAsync +.. autoprotocol:: toga.window.OnCloseHandlerGenerator +.. autoprotocol:: toga.window.DialogResultHandlerSync +.. autoprotocol:: toga.window.DialogResultHandlerAsync +.. autoprotocol:: toga.window.DialogResultHandlerGenerator diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index aa4837f1be..6992245f2c 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -3,6 +3,7 @@ accessors amongst App apps +Async awaitable backend backends diff --git a/pyproject.toml b/pyproject.toml index d721450568..6a4df01479 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,3 +72,36 @@ type = [ # We're not doing anything Python-related at the root level of the repo, but if this # declaration isn't here, tox commands run from the root directory raise a warning that # pyproject.toml doesn't contain a setuptools_scm section. + +[tool.mypy] +python_version = "3.8" +strict = true +show_error_context = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unreachable = true +disallow_any_unimported = true +warn_return_any = false # disable from strict=true +#disallow_any_explicit = true +#disallow_any_expr = true +#disallow_any_decorated = true + +# Specific overrides +[[tool.mypy.overrides]] +module = [ + "toga.style.*", + "toga.fonts", + "toga.widgets.base", + "toga.widgets.canvas", +] +disallow_subclassing_any = false +disallow_any_unimported = false + +# Ignore missing types for imports +[[tool.mypy.overrides]] +module = [ + "travertino.*", + "setuptools_scm", + "importlib_metadata", # alternatively, this could be installed always with [dev] +] +ignore_missing_imports = true diff --git a/tox.ini b/tox.ini index da319af80e..ec9fc1a3a7 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,15 @@ deps = {tox_root}{/}core[dev] commands = pre-commit run --all-files --show-diff-on-failure --color=always +[testenv:types] +skip_install = True +changedir = core +passenv = FORCE_COLOR +deps = + {tox_root}{/}core[dev] +commands = + mypy --color-output --config ../pyproject.toml src/ + # The leading comma generates the "py" environment [testenv:py{,38,39,310,311,312,313}{,-cov}] depends = pre-commit From 3395a1eb39f4a9824f3a95dbd6da92a4ee635b4b Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Fri, 5 Apr 2024 14:42:19 -0400 Subject: [PATCH 02/13] Drop mypy compliance --- .github/workflows/ci.yml | 30 --------- .gitignore | 1 - changes/2252.feature.rst | 1 + changes/2252.misc.rst | 1 - core/pyproject.toml | 2 +- core/src/toga/__init__.py | 2 +- core/src/toga/app.py | 49 +++++++------- core/src/toga/command.py | 6 +- core/src/toga/documents.py | 3 +- core/src/toga/handlers.py | 4 +- core/src/toga/hardware/camera.py | 5 +- core/src/toga/icons.py | 10 ++- core/src/toga/images.py | 13 ++-- core/src/toga/keys.py | 2 +- core/src/toga/paths.py | 4 +- core/src/toga/platform.py | 3 +- core/src/toga/screens.py | 2 +- core/src/toga/sources/accessors.py | 2 +- core/src/toga/sources/tree_source.py | 15 ++--- core/src/toga/style/pack.py | 2 +- core/src/toga/widgets/activityindicator.py | 2 +- core/src/toga/widgets/box.py | 2 +- core/src/toga/widgets/button.py | 8 +-- core/src/toga/widgets/canvas.py | 74 ++++++++++----------- core/src/toga/widgets/dateinput.py | 14 ++-- core/src/toga/widgets/detailedlist.py | 30 ++++----- core/src/toga/widgets/divider.py | 2 +- core/src/toga/widgets/imageview.py | 9 ++- core/src/toga/widgets/mapview.py | 13 ++-- core/src/toga/widgets/multilinetextinput.py | 8 +-- core/src/toga/widgets/numberinput.py | 26 ++++---- core/src/toga/widgets/optioncontainer.py | 62 +++++++---------- core/src/toga/widgets/progressbar.py | 6 +- core/src/toga/widgets/scrollcontainer.py | 14 ++-- core/src/toga/widgets/selection.py | 27 ++++---- core/src/toga/widgets/slider.py | 15 +++-- core/src/toga/widgets/splitcontainer.py | 6 +- core/src/toga/widgets/switch.py | 4 +- core/src/toga/widgets/table.py | 25 +++---- core/src/toga/widgets/textinput.py | 21 +++--- core/src/toga/widgets/timeinput.py | 14 ++-- core/src/toga/widgets/tree.py | 25 +++---- core/src/toga/widgets/webview.py | 11 +-- core/src/toga/window.py | 66 +++++------------- tox.ini | 9 --- 45 files changed, 279 insertions(+), 371 deletions(-) create mode 100644 changes/2252.feature.rst delete mode 100644 changes/2252.misc.rst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feff86b2ee..0a1127fbac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,36 +67,6 @@ jobs: build-subdirectory: ${{ matrix.subdir }} attest: ${{ inputs.attest-package }} - types: - name: Types - runs-on: ubuntu-latest - needs: [ pre-commit, towncrier, package ] - steps: - - name: Checkout Toga - uses: actions/checkout@v4.1.2 - - - name: Checkout beeware/.github - uses: actions/checkout@v4.1.2 - with: - repository: beeware/.github - path: .github - - - name: Set up Python ${{ env.min_python_version }} - uses: actions/setup-python@v5.0.0 - with: - python-version: ${{ env.min_python_version }} - - - name: Install tox - working-directory: .github/scripts - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade setuptools build wheel - # Utility script installs tox as defined in core/pyproject.toml - python -m install_requirement tox --extra dev --project-root ${{ github.workspace }}/core - - - name: Type Checks - run: tox -e types - core: name: Test core runs-on: ${{ matrix.platform }} diff --git a/.gitignore b/.gitignore index c47413608b..95f5bd0ed5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ pyvenv.cfg .envrc bin/ lib/ -.dmypy.json # Exclude briefcase packages from the examples dir. examples/*/macOS/ diff --git a/changes/2252.feature.rst b/changes/2252.feature.rst new file mode 100644 index 0000000000..42b21fd999 --- /dev/null +++ b/changes/2252.feature.rst @@ -0,0 +1 @@ +The typing for Toga's API surface was updated to be more precise. diff --git a/changes/2252.misc.rst b/changes/2252.misc.rst deleted file mode 100644 index 4a8852817f..0000000000 --- a/changes/2252.misc.rst +++ /dev/null @@ -1 +0,0 @@ -The typing for Toga's API surface is now compliant with mypy. diff --git a/core/pyproject.toml b/core/pyproject.toml index aa52cc3799..0c10c32271 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -76,7 +76,7 @@ dev = [ "pytest-freezer == 0.4.8", "setuptools-scm == 8.1.0", "tox == 4.15.0", - "types-Pillow == 10.1.0.2", + "types-Pillow == 10.1.0.2", # TODO:PR: does this help anything? # typing-extensions needed for TypeAlias added in Py 3.10 "typing-extensions == 4.9.0 ; python_version < '3.10'", ] diff --git a/core/src/toga/__init__.py b/core/src/toga/__init__.py index 391f199554..87b361a8d5 100644 --- a/core/src/toga/__init__.py +++ b/core/src/toga/__init__.py @@ -6,7 +6,7 @@ from .app import App, DocumentApp, DocumentMainWindow, MainWindow # Resources -from .colors import hsl, hsla, rgb, rgba # type: ignore[attr-defined] +from .colors import hsl, hsla, rgb, rgba from .command import Command, Group from .documents import Document from .fonts import Font diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 7246cc2166..dcdd8e47c2 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -261,7 +261,7 @@ def __init__( def _default_title(self) -> str: return App.app.formal_name - @property # type: ignore[override] + @property def on_close(self) -> None: """The handler to invoke before the window is closed in response to a user action. @@ -329,8 +329,6 @@ class App: #: The currently running :class:`~toga.App`. Since there can only be one running #: Toga app in a process, this is available as a class property via ``toga.App.app``. app: App - _impl: Any - _camera: Camera def __init__( self, @@ -390,7 +388,7 @@ def __init__( # 2023-10: Backwards compatibility ###################################################################### if id is not None: - warn( # type: ignore[unreachable] + warn( "App.id is deprecated and will be ignored. Use app_id instead", DeprecationWarning, stacklevel=2, @@ -450,7 +448,7 @@ def __init__( if formal_name: self._formal_name = formal_name else: - self._formal_name = self.metadata.get("Formal-Name") # type: ignore[assignment] + self._formal_name = self.metadata.get("Formal-Name") if self._formal_name is None: raise RuntimeError("Toga application must have a formal name") @@ -459,26 +457,30 @@ def __init__( if app_id: self._app_id = app_id else: - self._app_id = self.metadata.get("App-ID") # type: ignore[assignment] + self._app_id = self.metadata.get("App-ID") if self._app_id is None: raise RuntimeError("Toga application must have an app ID") # Other metadata may be passed to the constructor, or loaded with importlib. - self._author = author - if not self._author: - self._author = self.metadata.get("Author") + if author: + self._author = author + else: + self._author = self.metadata.get("Author", None) - self._version = version - if not self._version: - self._version = self.metadata.get("Version") + if version: + self._version = version + else: + self._version = self.metadata.get("Version", None) - self._home_page = home_page - if not self._home_page: - self._home_page = self.metadata.get("Home-page") + if home_page: + self._home_page = home_page + else: + self._home_page = self.metadata.get("Home-page", None) - self._description = description - if not self._description: - self._description = self.metadata.get("Summary") + if description: + self._description = description + else: + self._description = self.metadata.get("Summary", None) # Get a platform factory. self.factory = get_platform_factory() @@ -491,7 +493,7 @@ def __init__( else: self.icon = icon - self.on_exit = on_exit # type: ignore[assignment] + self.on_exit = on_exit # We need the command set to exist so that startup et al. can add commands; # but we don't have an impl yet, so we can't set the on_change handler @@ -564,8 +566,7 @@ def icon(self, icon_or_name: IconContent) -> None: if isinstance(icon_or_name, Icon): self._icon = icon_or_name else: - # TODO:PR: not valid for icon_or_name to be None - self._icon = Icon(icon_or_name) # type:ignore[arg-type] + self._icon = Icon(icon_or_name) try: self._impl.set_icon(self._icon) @@ -859,7 +860,7 @@ def name(self) -> str: return self._formal_name # Support WindowSet __iadd__ and __isub__ - @windows.setter # type:ignore[no-redef,attr-defined,misc] + @windows.setter def windows(self, windows: WindowSet) -> None: if windows is not self._windows: raise AttributeError("can't set attribute 'windows'") @@ -955,8 +956,6 @@ def _open(self, path: Path) -> None: except KeyError: raise ValueError(f"Don't know how to open documents of type {path.suffix}") else: - # TODO:PR: does this need `document_type`? or is `Document` not the right type? - # TODO:PR: revisit this once #2244 is merged - document = DocType(path, app=self) # type: ignore[call-arg] + document = DocType(path, app=self) self._documents.append(document) document.show() diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 7cc0759673..8ceeacf4ad 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterator, Protocol, Union, no_type_check +from collections.abc import Iterator +from typing import TYPE_CHECKING, Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.icons import Icon @@ -212,7 +213,7 @@ def __init__( self.shortcut = shortcut self.tooltip = tooltip - self.icon = icon # type: ignore[assignment] + self.icon = icon self.group = group self.section = section @@ -380,7 +381,6 @@ def descendant(group: Group, ancestor: Group) -> Group | None: # can't `peek` at the top element of an iterator, `push` an item back on after # it has been consumed, or pass the consumed item as a return value in addition # to the generator result. - @no_type_check def _iter_group(parent): nonlocal command nonlocal finished diff --git a/core/src/toga/documents.py b/core/src/toga/documents.py index 1a30f338d7..c107896249 100644 --- a/core/src/toga/documents.py +++ b/core/src/toga/documents.py @@ -102,8 +102,7 @@ def main_window(self, window: Window) -> None: def show(self) -> None: """Show the :any:`main_window` for this document.""" - # TODO:PR: does this need a None check for main_window? - self.main_window.show() # type: ignore[union-attr] + self.main_window.show() @abstractmethod def create(self) -> None: diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index d6e30a84d2..ea707247b1 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -135,7 +135,7 @@ def _handler(*args: object, **kwargs: object) -> object: traceback.print_exc() return None - _handler._raw = handler # type: ignore[attr-defined] + _handler._raw = handler else: # A dummy no-op handler @@ -148,7 +148,7 @@ def _handler(*args: object, **kwargs: object) -> object: traceback.print_exc() return None - _handler._raw = None # type: ignore[attr-defined] + _handler._raw = None return _handler diff --git a/core/src/toga/hardware/camera.py b/core/src/toga/hardware/camera.py index 91e3fd5c6b..d0676727d0 100644 --- a/core/src/toga/hardware/camera.py +++ b/core/src/toga/hardware/camera.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from toga.app import App + from toga.widgets.base import Widget class PhotoResult(AsyncResult): @@ -33,8 +34,8 @@ def has_flash(self) -> bool: """Does the device have a flash?""" return self._impl.has_flash() - def __eq__(self, other: object) -> bool: - return self.id == other.id # type:ignore[attr-defined] + def __eq__(self, other: Widget) -> bool: + return self.id == other.id def __repr__(self) -> str: return f"" diff --git a/core/src/toga/icons.py b/core/src/toga/icons.py index db8ddc7cfa..4cfd0f5d8e 100644 --- a/core/src/toga/icons.py +++ b/core/src/toga/icons.py @@ -1,15 +1,16 @@ from __future__ import annotations import warnings +from collections.abc import Callable, Iterable from pathlib import Path -from typing import TYPE_CHECKING, Callable, Iterable, Union +from typing import TYPE_CHECKING import toga from toga.platform import get_platform_factory from toga.types import TypeAlias if TYPE_CHECKING: - IconContent: TypeAlias = Union[str, Path, toga.Icon] + IconContent: TypeAlias = str | Path | toga.Icon class cachedicon: @@ -99,10 +100,7 @@ def __init__( self.system = system if self.system: - resource_path = ( - Path(self.factory.__file__).parent # type:ignore[arg-type] - / "resources" - ) + resource_path = Path(self.factory.__file__).parent / "resources" else: resource_path = toga.App.app.paths.app diff --git a/core/src/toga/images.py b/core/src/toga/images.py index e91dc44ba5..1b8f24c65e 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -5,7 +5,7 @@ import warnings from functools import lru_cache from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol, Union +from typing import TYPE_CHECKING, Any, Protocol from warnings import warn import toga @@ -27,10 +27,10 @@ ImageT = TypeVar("ImageT") # Define the types that can be used as Image content - PathLike: TypeAlias = Union[str, Path] - BytesLike: TypeAlias = Union[bytes, bytearray, memoryview] + PathLike: TypeAlias = str | Path + BytesLike: TypeAlias = bytes | bytearray | memoryview ImageLike: TypeAlias = Any - ImageContent: TypeAlias = Union[PathLike, BytesLike, ImageLike] + ImageContent: TypeAlias = PathLike | BytesLike | ImageLike # Define a type variable representing an image of an externally defined type. ExternalImageT = TypeVar("ExternalImageT", bound=object) @@ -41,9 +41,8 @@ class ImageConverter(Protocol): :any:`toga.Image`. """ - # TODO:PR: figure out how to resolve mypy issues #: The base image class this plugin can interpret. - image_class: type[ExternalImageT] # type:ignore[valid-type] + image_class: type[ExternalImageT] @staticmethod def convert_from_format(image_in_format: ExternalImageT) -> BytesLike: @@ -213,7 +212,7 @@ def as_format(self, format: type[ImageT]) -> ImageT: """ if isinstance(format, type): if issubclass(format, Image): - return format(self.data) # type:ignore[return-value] + return format(self.data) for converter in self._converters(): if issubclass(format, converter.image_class): diff --git a/core/src/toga/keys.py b/core/src/toga/keys.py index efc1c20046..4e518cb50c 100644 --- a/core/src/toga/keys.py +++ b/core/src/toga/keys.py @@ -171,7 +171,7 @@ def __add__(self, other: Key | str) -> str: """ try: # Try Key + Key - return self.value + other.value # type: ignore[union-attr] + return self.value + other.value except AttributeError: return self.value + other diff --git a/core/src/toga/paths.py b/core/src/toga/paths.py index b82bc28d41..b73fa03924 100644 --- a/core/src/toga/paths.py +++ b/core/src/toga/paths.py @@ -7,7 +7,7 @@ class Paths: - def __init__(self) -> None: + def __init__(self): self.factory = get_platform_factory() self._impl = self.factory.Paths(self) @@ -28,7 +28,7 @@ def app(self) -> Path: files into this path. """ try: - return Path(importlib.util.find_spec(toga.App.app.__module__).origin).parent # type: ignore + return Path(importlib.util.find_spec(toga.App.app.__module__).origin).parent except ValueError: # When running a single file `python path/to/myapp.py`, the app # won't have a module because it's the mainline. Default to the diff --git a/core/src/toga/platform.py b/core/src/toga/platform.py index 2b20eae696..d950e29149 100644 --- a/core/src/toga/platform.py +++ b/core/src/toga/platform.py @@ -62,8 +62,7 @@ def get_platform_factory() -> ModuleType: :returns: The factory for the host platform. """ - backend_value = os.environ.get("TOGA_BACKEND") - if backend_value: + if backend_value := os.environ.get("TOGA_BACKEND"): try: factory = importlib.import_module(f"{backend_value}.factory") except ModuleNotFoundError as e: diff --git a/core/src/toga/screens.py b/core/src/toga/screens.py index b13411c7d4..8829522fe3 100644 --- a/core/src/toga/screens.py +++ b/core/src/toga/screens.py @@ -29,7 +29,7 @@ def size(self) -> tuple[int, int]: """The size of the screen, as a ``(width, height)`` tuple.""" return self._impl.get_size() - def as_image(self, format: type[ImageT] = Image) -> ImageT: # type: ignore[assignment] + def as_image(self, format: type[ImageT] = Image) -> ImageT: """Render the current contents of the screen as an image. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also diff --git a/core/src/toga/sources/accessors.py b/core/src/toga/sources/accessors.py index 1e73ae9612..9824f31238 100644 --- a/core/src/toga/sources/accessors.py +++ b/core/src/toga/sources/accessors.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import Collection, Mapping +from collections.abc import Collection, Mapping NON_ACCESSOR_CHARS = re.compile(r"[^\w ]") WHITESPACE = re.compile(r"\s+") diff --git a/core/src/toga/sources/tree_source.py b/core/src/toga/sources/tree_source.py index fc569f3dc4..2329a38ef4 100644 --- a/core/src/toga/sources/tree_source.py +++ b/core/src/toga/sources/tree_source.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Generic, Iterable, Iterator, Mapping, Tuple, TypeVar, Union +from collections.abc import Iterator +from typing import Generic, Iterable, Mapping, Tuple, TypeVar, Union from toga.types import TypeAlias @@ -62,8 +63,7 @@ def __delitem__(self, index: int) -> None: # Child isn't part of this source, or a child of this node anymore. child._parent = None - # TODO:PR: consider del? - child._source = None # type: ignore[assignment] + child._source = None self._source.notify("remove", parent=self, index=index, item=child) @@ -101,8 +101,7 @@ def __setitem__(self, index: int, data: T) -> None: old_node = self._children[index] old_node._parent = None - # TODO:PR: consider del? - old_node._source = None # type: ignore[assignment] + old_node._source = None node = self._source._create_node(parent=self, data=data) self._children[index] = node @@ -237,8 +236,7 @@ def __getitem__(self, index: int) -> Node[T]: def __delitem__(self, index: int) -> None: node = self._roots[index] del self._roots[index] - # TODO:PR: consider del? - node._source = None # type: ignore[assignment] + node._source = None self.notify("remove", parent=None, index=index, item=node) ###################################################################### @@ -297,8 +295,7 @@ def __setitem__(self, index: int, data: object) -> None: """ old_root = self._roots[index] old_root._parent = None - # TODO:PR: consider del? - old_root._source = None # type: ignore[assignment] + old_root._source = None root = self._create_node(parent=None, data=data) self._roots[index] = root diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 8978ba79af..8743293968 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -15,7 +15,7 @@ LEFT, LTR, MONOSPACE, - NONE as NONE, + NONE, NORMAL, OBLIQUE, RIGHT, diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index f4d70d6d4b..84e1b954b3 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -29,7 +29,7 @@ def __init__( if running: self.start() - @property # type: ignore[override] + @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/box.py b/core/src/toga/widgets/box.py index 7733378721..a7c71e056a 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable +from collections.abc import Iterable from toga.style import Pack diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index 1c4f9a7d36..5d5a2b0f23 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -67,18 +67,18 @@ def __init__( # 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 # type: ignore[assignment] + self.on_press = None # Set the content of the button - either an icon, or text, but not both. if icon: if text is not None: raise ValueError("Cannot specify both text and an icon") else: - self.icon = icon # type:ignore[assignment] + self.icon = icon else: - self.text = text # type:ignore[assignment] + self.text = text - self.on_press = on_press # type: ignore[assignment] + self.on_press = on_press self.enabled = enabled @property diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 9cad86144c..1c5c1f5f54 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -2,13 +2,13 @@ import warnings from abc import ABC, abstractmethod +from collections.abc import Iterator from contextlib import contextmanager from math import cos, pi, sin, tan from typing import ( TYPE_CHECKING, Any, ContextManager, - Iterator, Literal, NoReturn, Protocol, @@ -17,9 +17,9 @@ from travertino.colors import Color import toga -from toga.colors import BLACK, color as parse_color # type: ignore[attr-defined] +from toga.colors import BLACK, color as parse_color from toga.constants import Baseline, FillRule -from toga.fonts import ( # type: ignore[attr-defined] +from toga.fonts import ( SYSTEM, SYSTEM_DEFAULT_FONT_SIZE, Font, @@ -370,7 +370,7 @@ def __init__( self.text = text self.x = x self.y = y - self.font = font # type: ignore[assignment] + self.font = font self.baseline = baseline def __repr__(self) -> str: @@ -722,7 +722,7 @@ def fill( :returns: The ``Fill`` :any:`DrawingObject` for the operation. """ if preserve is not None: - warnings.warn( # type: ignore[unreachable] + warnings.warn( "The `preserve` argument on fill() has been deprecated.", DeprecationWarning, ) @@ -955,14 +955,14 @@ def new_path(self) -> BeginPath: ) return self.begin_path() - def context(self): # type: ignore + def context(self): """**DEPRECATED** - use :meth:`~toga.widgets.canvas.Context.Context`""" warnings.warn( "Context.context() has been renamed Context.Context()", DeprecationWarning ) return self.Context() - def closed_path(self, x: float, y: float): # type: ignore + def closed_path(self, x: float, y: float): """**DEPRECATED** - use :meth:`~toga.widgets.canvas.Context.ClosedPath`""" warnings.warn( "Context.closed_path() has been renamed Context.ClosedPath()", @@ -1244,16 +1244,16 @@ def __init__( self._impl = self.factory.Canvas(interface=self) # Set all the properties - self.on_resize = on_resize # type: ignore[assignment] - self.on_press = on_press # type: ignore[assignment] - self.on_activate = on_activate # type: ignore[assignment] - self.on_release = on_release # type: ignore[assignment] - self.on_drag = on_drag # type: ignore[assignment] - self.on_alt_press = on_alt_press # type: ignore[assignment] - self.on_alt_release = on_alt_release # type: ignore[assignment] - self.on_alt_drag = on_alt_drag # type: ignore[assignment] - - @property # type: ignore[override] + self.on_resize = on_resize + self.on_press = on_press + self.on_activate = on_activate + self.on_release = on_release + self.on_drag = on_drag + self.on_alt_press = on_alt_press + self.on_alt_release = on_alt_release + self.on_alt_drag = on_alt_drag + + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? ScrollContainer widgets cannot be disabled; this property will always return @@ -1328,7 +1328,7 @@ def Fill( :param color: The fill color. :yields: The new :class:`~toga.widgets.canvas.FillContext` context object. """ - return self.context.Fill(x, y, color, fill_rule) # type: ignore[arg-type] + return self.context.Fill(x, y, color, fill_rule) def Stroke( self, @@ -1352,7 +1352,7 @@ def Stroke( solid line. :yields: The new :class:`~toga.widgets.canvas.StrokeContext` context object. """ - return self.context.Stroke(x, y, color, line_width, line_dash) # type: ignore[arg-type] + return self.context.Stroke(x, y, color, line_width, line_dash) @property def on_resize(self) -> WrappedHandlerT: @@ -1461,7 +1461,7 @@ def measure_text( :returns: A tuple of ``(width, height)``. """ if tight is not None: - warnings.warn( # type: ignore[unreachable] + warnings.warn( "The `tight` argument on Canvas.measure_text() has been deprecated.", DeprecationWarning, ) @@ -1474,7 +1474,7 @@ def measure_text( # As image ########################################################################### - def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: # type: ignore[assignment] + def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: """Render the canvas as an image. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also @@ -1489,7 +1489,7 @@ def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: # type: ignore # 2023-07 Backwards compatibility ########################################################################### - def new_path(self): # type: ignore + def new_path(self): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.begin_path` on :attr:`context`""" warnings.warn( @@ -1498,7 +1498,7 @@ def new_path(self): # type: ignore ) return self.context.begin_path() - def move_to(self, x, y): # type: ignore + def move_to(self, x, y): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.move_to` on :attr:`context`""" warnings.warn( @@ -1507,7 +1507,7 @@ def move_to(self, x, y): # type: ignore ) return self.context.move_to(x, y) - def line_to(self, x, y): # type: ignore + def line_to(self, x, y): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.line_to` on :attr:`context`""" warnings.warn( @@ -1516,7 +1516,7 @@ def line_to(self, x, y): # type: ignore ) return self.context.line_to(x, y) - def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y): # type: ignore + def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.bezier_curve_to` on :attr:`context`""" warnings.warn( @@ -1525,7 +1525,7 @@ def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y): # type: ignore ) return self.context.bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y) - def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float): # type: ignore + def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.quadratic_curve_to` on :attr:`context`""" warnings.warn( @@ -1534,7 +1534,7 @@ def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float): # typ ) return self.context.quadratic_curve_to(cpx, cpy, x, y) - def arc(self, x, y, radius, startangle=0.0, endangle=2 * pi, anticlockwise=False): # type: ignore + def arc(self, x, y, radius, startangle=0.0, endangle=2 * pi, anticlockwise=False): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.arc` on :attr:`context`""" warnings.warn( @@ -1543,7 +1543,7 @@ def arc(self, x, y, radius, startangle=0.0, endangle=2 * pi, anticlockwise=False ) return self.context.arc(x, y, radius, startangle, endangle, anticlockwise) - def ellipse( # type: ignore + def ellipse( self, x: float, y: float, @@ -1571,7 +1571,7 @@ def ellipse( # type: ignore anticlockwise, ) - def rect(self, x: float, y: float, width: float, height: float): # type: ignore + def rect(self, x: float, y: float, width: float, height: float): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.rect` on :attr:`context`""" warnings.warn( @@ -1580,7 +1580,7 @@ def rect(self, x: float, y: float, width: float, height: float): # type: ignore ) return self.context.rect(x, y, width, height) - def write_text(self, text: str, x=0, y=0, font=None): # type: ignore + def write_text(self, text: str, x=0, y=0, font=None): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.write_text` on :attr:`context`""" warnings.warn( @@ -1589,7 +1589,7 @@ def write_text(self, text: str, x=0, y=0, font=None): # type: ignore ) return self.context.write_text(text, x, y, font) - def rotate(self, radians: float): # type: ignore + def rotate(self, radians: float): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.rotate` on :attr:`context`""" warnings.warn( @@ -1598,7 +1598,7 @@ def rotate(self, radians: float): # type: ignore ) return self.context.rotate(radians) - def scale(self, sx: float, sy: float): # type: ignore + def scale(self, sx: float, sy: float): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.scale` on :attr:`context`""" warnings.warn( "Direct canvas operations have been deprecated; use context.scale()", @@ -1606,7 +1606,7 @@ def scale(self, sx: float, sy: float): # type: ignore ) return self.context.scale(sx, sy) - def translate(self, tx: float, ty: float): # type: ignore + def translate(self, tx: float, ty: float): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.translate` on :attr:`context`""" warnings.warn( @@ -1615,7 +1615,7 @@ def translate(self, tx: float, ty: float): # type: ignore ) return self.context.translate(tx, ty) - def reset_transform(self): # type: ignore + def reset_transform(self): """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.reset_transform` on :attr:`context`""" warnings.warn( @@ -1624,7 +1624,7 @@ def reset_transform(self): # type: ignore ) return self.context.reset_transform() - def closed_path(self, x, y): # type: ignore + def closed_path(self, x, y): """**DEPRECATED** - use :meth:`~toga.Canvas.ClosedPath`""" warnings.warn( "Canvas.closed_path() has been renamed Canvas.ClosedPath()", @@ -1632,7 +1632,7 @@ def closed_path(self, x, y): # type: ignore ) return self.ClosedPath(x, y) - def fill( # type: ignore + def fill( self, color: Color | str | None = BLACK, fill_rule: FillRule = FillRule.NONZERO, @@ -1650,7 +1650,7 @@ def fill( # type: ignore ) return self.Fill(color=color, fill_rule=fill_rule) - def stroke( # type: ignore + def stroke( self, color: Color | str | None = BLACK, line_width: float = 2.0, diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index c742be9175..49e16feafe 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -68,12 +68,12 @@ def __init__( # Create a platform specific implementation of a DateInput self._impl = self.factory.DateInput(interface=self) - self.on_change = None # type: ignore[assignment] - self.min = min # type: ignore[assignment] - self.max = max # type: ignore[assignment] + self.on_change = None + self.min = min + self.max = max - self.value = value # type: ignore[assignment] - self.on_change = on_change # type: ignore[assignment] + self.value = value + self.on_change = on_change @property def value(self) -> datetime.date: @@ -212,7 +212,7 @@ def min_date(self, value: object) -> None: warnings.warn( "DatePicker.min_date has been renamed DateInput.min", DeprecationWarning ) - self.min = value # type: ignore[assignment] + self.min = value @property def max_date(self) -> datetime.date: @@ -226,4 +226,4 @@ def max_date(self, value: object) -> None: warnings.warn( "DatePicker.max_date has been renamed DateInput.max", DeprecationWarning ) - self.max = value # type: ignore[assignment] + self.max = value diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 5331f92ee8..8a976e5013 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -1,10 +1,10 @@ from __future__ import annotations import warnings +from collections.abc import Iterable from typing import ( Any, Generic, - Iterable, Literal, Protocol, TypeVar, @@ -152,7 +152,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if on_delete: - if on_primary_action: # type: ignore[unreachable] + if on_primary_action: raise ValueError("Cannot specify both on_delete and on_primary_action") else: warnings.warn( @@ -169,20 +169,20 @@ def __init__( self._missing_value = missing_value self._primary_action = primary_action self._secondary_action = secondary_action - self.on_select = None # type: ignore[assignment] + self.on_select = None # TODO:PR: in reality, _data needs to be Sized and SupportsIndex... - self._data: SourceT | ListSource[T] = None # type: ignore[assignment] + self._data: SourceT | ListSource[T] = None self._impl = self.factory.DetailedList(interface=self) - self.data = data # type: ignore[assignment] - self.on_primary_action = on_primary_action # type: ignore[assignment] - self.on_secondary_action = on_secondary_action # type: ignore[assignment] - self.on_refresh = on_refresh # type: ignore[assignment] - self.on_select = on_select # type: ignore[assignment] + self.data = data + self.on_primary_action = on_primary_action + self.on_secondary_action = on_secondary_action + self.on_refresh = on_refresh + self.on_select = on_select - @property # type: ignore[override] + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? DetailedList widgets cannot be disabled; this property will always return True; any @@ -219,7 +219,7 @@ def data(self, data: SourceT | Iterable[T] | None) -> None: if data is None: self._data = ListSource(data=[], accessors=self.accessors) elif isinstance(data, Source): - self._data = data # type: ignore[assignment] + self._data = data else: self._data = ListSource(data=data, accessors=self.accessors) @@ -236,11 +236,11 @@ def scroll_to_row(self, row: int) -> None: :param row: The index of the row to make visible. Negative values refer to the nth last row (-1 is the last row, -2 second last, and so on). """ - if len(self.data) > 1: # type: ignore[arg-type] + if len(self.data) > 1: if row >= 0: - self._impl.scroll_to_row(min(row, len(self.data))) # type: ignore[arg-type] + self._impl.scroll_to_row(min(row, len(self.data))) else: - self._impl.scroll_to_row(max(len(self.data) + row, 0)) # type: ignore[arg-type] + self._impl.scroll_to_row(max(len(self.data) + row, 0)) def scroll_to_bottom(self) -> None: """Scroll the view so that the bottom of the list (last row) is visible.""" @@ -265,7 +265,7 @@ def selection(self) -> Row[T] | None: Returns the selected Row object, or :any:`None` if no row is currently selected. """ try: - return self.data[self._impl.get_selection()] # type: ignore[index] + return self.data[self._impl.get_selection()] except TypeError: return None diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index 2469642bce..adec0acd47 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -34,7 +34,7 @@ def __init__( self._impl = self.factory.Divider(interface=self) self.direction = direction - @property # type: ignore[override] + @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 2f0c43c0b1..a5d4032afc 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -89,9 +89,9 @@ def __init__( # Prime the image attribute self._image = None self._impl = self.factory.ImageView(interface=self) - self.image = image # type: ignore[assignment] + self.image = image - @property # type: ignore[override] + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? @@ -129,7 +129,7 @@ def image(self, image: ImageContent) -> None: self._impl.set_image(self._image) self.refresh() - def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: # type: ignore[assignment] + def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: """Return the image in the specified format. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also @@ -138,5 +138,4 @@ def as_image(self, format: type[ImageT] = toga.Image) -> ImageT: # type: ignore `. :returns: The image in the specified format. """ - # TODO:PR: what's the use-case for initializing with image=None? cause this won't work then... - return self.image.as_format(format) # type: ignore[union-attr] + return self.image.as_format(format) diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 97755db5bc..23425d8875 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Iterator, Protocol, Union +from collections.abc import Iterator +from typing import Any, Protocol, Union import toga from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler @@ -29,7 +30,7 @@ def __init__( self._subtitle = subtitle # A pin isn't tied to a map at time of creation. - self.interface: MapView = None # type:ignore[assignment] + self.interface: MapView = None self._native = None def __repr__(self) -> str: @@ -112,7 +113,7 @@ def remove(self, pin: MapPin) -> None: """ self.interface._impl.remove_pin(pin) self._pins.remove(pin) - pin.interface = None # type:ignore[assignment] + pin.interface = None def clear(self) -> None: """Remove all pins from the map.""" @@ -177,14 +178,14 @@ def __init__( self._pins = MapPinSet(self, pins) if location: - self.location = location # type:ignore[assignment] + self.location = location else: # Default location is Perth, Australia. Because why not? - self.location = (-31.9559, 115.8606) # type:ignore[assignment] + self.location = (-31.9559, 115.8606) self.zoom = zoom - self.on_select = on_select # type:ignore[assignment] + self.on_select = on_select @property def location(self) -> toga.LatLng: diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 564f7ec69e..2634c57ca8 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -59,13 +59,13 @@ def __init__( # 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 # type: ignore[assignment] - self.value = value # type: ignore[assignment] + self.on_change = None + self.value = value # Set all the properties self.readonly = readonly - self.placeholder = placeholder # type: ignore[assignment] - self.on_change = on_change # type: ignore[assignment] + self.placeholder = placeholder + self.on_change = on_change @property def placeholder(self) -> str: diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index e9636746f8..9c09af3cda 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -125,7 +125,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if min_value is not None: - if min is not None: # type: ignore[unreachable] + if min is not None: raise ValueError("Cannot specify both min and min_value") else: warnings.warn( @@ -134,7 +134,7 @@ def __init__( ) min = min_value if max_value is not None: - if max is not None: # type: ignore[unreachable] + if max is not None: raise ValueError("Cannot specify both max and max_value") else: warnings.warn( @@ -152,16 +152,16 @@ def __init__( self._min: Decimal | None = None self._max: Decimal | None = None - self.on_change = None # type: ignore[assignment] + self.on_change = None self._impl = self.factory.NumberInput(interface=self) self.readonly = readonly - self.step = step # type: ignore[assignment] - self.min = min # type: ignore[assignment] - self.max = max # type: ignore[assignment] - self.value = value # type: ignore[assignment] + self.step = step + self.min = min + self.max = max + self.value = value - self.on_change = on_change # type: ignore[assignment] + self.on_change = on_change @property def readonly(self) -> bool: @@ -212,7 +212,7 @@ def min(self) -> Decimal | None: @min.setter def min(self, new_min: NumberInputT | None) -> None: try: - new_min = _clean_decimal(new_min, self.step) # type: ignore[arg-type] + new_min = _clean_decimal(new_min, self.step) # Clip widget's value to the new minimum if self.value is not None and self.value < new_min: @@ -244,7 +244,7 @@ def max(self) -> Decimal | None: @max.setter def max(self, new_max: NumberInputT | None) -> None: try: - new_max = _clean_decimal(new_max, self.step) # type: ignore[arg-type] + new_max = _clean_decimal(new_max, self.step) # Clip widget's value to the new maximum if self.value is not None and self.value > new_max: @@ -291,7 +291,7 @@ def value(self) -> Decimal | None: @value.setter def value(self, value: NumberInputT | None) -> None: try: - value = _clean_decimal(value, self.step) # type: ignore[arg-type] + value = _clean_decimal(value, self.step) if self.min is not None and value < self.min: value = self.min @@ -334,7 +334,7 @@ def min_value(self, value: NumberInputT | None) -> None: "NumberInput.min_value has been renamed NumberInput.min", DeprecationWarning, ) - self.min = value # type: ignore[assignment] + self.min = value @property def max_value(self) -> Decimal | None: @@ -351,4 +351,4 @@ def max_value(self, value: NumberInputT | None) -> None: "NumberInput.max_value has been renamed NumberInput.max", DeprecationWarning, ) - self.max = value # type: ignore[assignment] + self.max = value diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 0ad581701a..2941b1f5e2 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -1,13 +1,11 @@ from __future__ import annotations +from collections.abc import Iterable from typing import ( TYPE_CHECKING, Any, - Iterable, Protocol, - Tuple, Union, - no_type_check, overload, ) @@ -22,12 +20,12 @@ if TYPE_CHECKING: from toga.icons import IconContent - OptionContainerContent: TypeAlias = Union[ - Tuple[str, Widget], - Tuple[str, Widget, Union[IconContent, None]], - Tuple[str, Widget, Union[IconContent, None], bool], - toga.OptionItem, - ] + OptionContainerContent: TypeAlias = ( + tuple[str, Widget] + | tuple[str, Widget, IconContent | None] + | tuple[str, Widget, IconContent | None, bool] + | toga.OptionItem + ) class OnSelectHandlerSync(Protocol): @@ -75,18 +73,18 @@ def __init__( # will become the source of truth. Initially prime the attributes with None (so # that the attribute exists), then use the setter to enforce validation on the # provided values. - self._text: str = None # type:ignore[assignment] - self._icon: toga.Icon = None # type:ignore[assignment] - self._enabled: bool = None # type:ignore[assignment] + self._text: str = None + self._icon: toga.Icon = None + self._enabled: bool = None self.text = text - self.icon = icon # type:ignore[assignment] + self.icon = icon self.enabled = enabled # Prime the attributes for properties that will be set when the OptionItem is # set as content. - self._interface: OptionContainer = None # type:ignore[assignment] - self._index: int = None # type:ignore[assignment] + self._interface: OptionContainer = None + self._index: int = None @property def interface(self) -> OptionContainer: @@ -165,7 +163,7 @@ def icon(self, icon_or_name: IconContent | None) -> None: icon = toga.Icon(icon_or_name) if hasattr(self, "_icon"): - self._icon = icon # type:ignore[assignment] + self._icon = icon else: self._interface._impl.set_option_icon(self.index, icon) @@ -189,8 +187,8 @@ def _preserve_option(self) -> None: self._enabled = self.enabled # Clear - self._index = None # type:ignore[assignment] - self._interface = None # type:ignore[assignment] + self._index = None + self._interface = None def _add_as_option(self, index: int, interface: OptionContainer) -> None: text = self._text @@ -218,7 +216,7 @@ def __init__(self, interface: Any): self._options: list[OptionItem] = [] def __repr__(self) -> str: - items = ", ".join(repr(option.text) for option in self) # type: ignore[attr-defined] + items = ", ".join(repr(option.text) for option in self) return f"" def __getitem__(self, index: int | str | OptionItem) -> OptionItem: @@ -271,10 +269,10 @@ def index(self, value: str | int | OptionItem) -> int: if isinstance(value, int): return value elif isinstance(value, OptionItem): - return value.index # type:ignore[return-value] + return value.index else: try: - return next(filter(lambda item: item.text == str(value), self)).index # type: ignore + return next(filter(lambda item: item.text == str(value), self)).index except StopIteration: raise ValueError(f"No tab named {value!r}") @@ -314,13 +312,7 @@ def append( :param icon: The :any:`icon content ` to use to represent the tab. :param enabled: Should the new tab be enabled? (Default: ``True``) """ - self.insert( # type:ignore[misc] - len(self), - text_or_item, # type:ignore[arg-type] - content, # type:ignore[arg-type] - icon=icon, - enabled=enabled, - ) + self.insert(len(self), text_or_item, content, icon=icon, enabled=enabled) @overload def insert( @@ -378,7 +370,7 @@ def insert( # Create an interface wrapper for the option. item = OptionItem( text_or_item, - content, # type:ignore[arg-type] + content, icon=icon, enabled=enabled if enabled is not None else True, ) @@ -416,22 +408,20 @@ def __init__( """ super().__init__(id=id, style=style) self._content = OptionList(self) - self.on_select = None # type: ignore[assignment] + self.on_select = None self._impl = self.factory.OptionContainer(interface=self) if content: for item in content: - # TODO:PR: OptionItem is not a widget...but iterating content returns widgets... if isinstance(item, OptionItem): - self.content.append(item) # type:ignore[unreachable] + self.content.append(item) else: if len(item) == 2: text, widget = item icon = None enabled = True - # TODO:PR: type of content is Iterable of tuples soooo.... - elif len(item) == 3: # type:ignore[unreachable] + elif len(item) == 3: text, widget, icon = item enabled = True elif len(item) == 4: @@ -445,7 +435,7 @@ def __init__( self.content.append(text, widget, enabled=enabled, icon=icon) - self.on_select = on_select # type: ignore[assignment] + self.on_select = on_select @property def enabled(self) -> bool: @@ -489,7 +479,6 @@ def current_tab(self, value: OptionItem | str | int) -> None: self._impl.set_current_tab_index(index) @Widget.app.setter - @no_type_check def app(self, app) -> None: # Invoke the superclass property setter Widget.app.fset(self, app) @@ -499,7 +488,6 @@ def app(self, app) -> None: item._content.app = app @Widget.window.setter - @no_type_check def window(self, window) -> None: # Invoke the superclass property setter Widget.window.fset(self, window) diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index e4d1e77a2b..be77fdf1d0 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -36,13 +36,13 @@ def __init__( self._impl = self.factory.ProgressBar(interface=self) - self.max = max # type: ignore[assignment] - self.value = value # type: ignore[assignment] + self.max = max + self.value = value if running: self.start() - @property # type: ignore[override] + @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 a929c0dd39..4409bc435b 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal, Protocol, SupportsInt, Union, no_type_check +from typing import Literal, Protocol, SupportsInt, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.style import Pack @@ -52,7 +52,7 @@ def __init__( super().__init__(id=id, style=style) self._content: Widget | None = None - self.on_scroll = None # type: ignore[assignment] + self.on_scroll = None # Create a platform specific implementation of a Scroll Container self._impl = self.factory.ScrollContainer(interface=self) @@ -61,10 +61,9 @@ def __init__( self.vertical = vertical self.horizontal = horizontal self.content = content - self.on_scroll = on_scroll # type: ignore[assignment] + self.on_scroll = on_scroll @Widget.app.setter - @no_type_check def app(self, app) -> None: # Invoke the superclass property setter Widget.app.fset(self, app) @@ -74,7 +73,6 @@ def app(self, app) -> None: self._content.app = app @Widget.window.setter - @no_type_check def window(self, window) -> None: # Invoke the superclass property setter Widget.window.fset(self, window) @@ -83,7 +81,7 @@ def window(self, window) -> None: if self._content: self._content.window = window - @property # type: ignore[override] + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? @@ -182,7 +180,7 @@ def horizontal_position(self, horizontal_position: SupportsInt) -> None: "Cannot set horizontal position when horizontal scrolling is not enabled." ) - self.position = (horizontal_position, self._impl.get_vertical_position()) # type: ignore[assignment] + self.position = (horizontal_position, self._impl.get_vertical_position()) @property def max_vertical_position(self) -> int: @@ -212,7 +210,7 @@ def vertical_position(self, vertical_position: SupportsInt) -> None: "Cannot set vertical position when vertical scrolling is not enabled." ) - self.position = (self._impl.get_horizontal_position(), vertical_position) # type: ignore[assignment] + self.position = (self._impl.get_horizontal_position(), vertical_position) # This combined property is necessary because on some platforms (e.g. iOS), setting # the horizontal and vertical position separately would cause the horizontal and diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 2e65a3b635..c99548681e 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -1,7 +1,8 @@ from __future__ import annotations import warnings -from typing import Any, Generic, Iterable, Protocol, TypeVar, Union +from collections.abc import Iterable +from typing import Any, Generic, Protocol, TypeVar, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Source @@ -66,7 +67,7 @@ def __init__( # 2023-05: Backwards compatibility ###################################################################### if on_select: # pragma: no cover - if on_change: # type: ignore[unreachable] + if on_change: raise ValueError("Cannot specify both on_select and on_change") else: warnings.warn( @@ -81,15 +82,15 @@ def __init__( self._items: SourceT | ListSource[T] self._on_change: WrappedHandlerT - self.on_change = None # type: ignore[assignment] # needed for _impl initialization + self.on_change = None # needed for _impl initialization self._impl = self.factory.Selection(interface=self) self._accessor = accessor - self.items = items # type: ignore[assignment] + self.items = items if value: self.value = value - self.on_change = on_change # type: ignore[assignment] + self.on_change = on_change self.enabled = enabled @property @@ -121,7 +122,7 @@ def items(self, items: SourceT | Iterable[T] | None) -> None: elif isinstance(items, Source): if self._accessor is None: raise ValueError("Must specify an accessor to use a data source") - self._items = items # type: ignore[assignment] + self._items = items else: self._items = ListSource(accessors=accessors, data=items) @@ -129,11 +130,11 @@ def items(self, items: SourceT | Iterable[T] | None) -> None: # Temporarily halt notifications orig_on_change = self._on_change - self.on_change = None # type: ignore[assignment] + self.on_change = None # Clear the widget, and insert all the data rows self._impl.clear() - for index, item in enumerate(self.items): # type: ignore[arg-type,var-annotated] + for index, item in enumerate(self.items): self._impl.insert(index, item) # Restore the original change handler and trigger it. @@ -172,22 +173,22 @@ def value(self) -> T | None: if index is None: return None - item = self._items[index] # type: ignore[index] + item = self._items[index] # If there was no accessor specified, the data values are literals. # Dereference the value out of the Row object. if item and self._accessor is None: - return item.value # type: ignore[union-attr] - return item # type: ignore[return-value] + return item.value + return item @value.setter def value(self, value: T) -> None: try: if self._accessor is None: - item = self._items.find(dict(value=value)) # type: ignore[union-attr] + item = self._items.find(dict(value=value)) else: item = value - index = self._items.index(item) # type: ignore[union-attr] + index = self._items.index(item) self._impl.select_item(index=index, item=item) except ValueError: raise ValueError(f"{value!r} is not a current item in the selection") diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index b2809ec496..82b6fc5d18 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -2,8 +2,9 @@ import warnings from abc import ABC, abstractmethod +from collections.abc import Iterator from contextlib import contextmanager -from typing import Any, Iterator, Protocol, SupportsFloat, Union +from typing import Any, Protocol, SupportsFloat, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.style import Pack @@ -112,7 +113,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if range is not None: - if min is not None or max is not None: # type: ignore[unreachable] + if min is not None or max is not None: raise ValueError( "range cannot be specified if min and max are specified" ) @@ -136,7 +137,7 @@ def __init__( # 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 # type: ignore[assignment] + self.on_change = None self.min = min self.max = max self.tick_count = tick_count @@ -144,9 +145,9 @@ def __init__( value = (min + max) / 2 self.value = value - self.on_change = on_change # type: ignore[assignment] - self.on_press = on_press # type: ignore[assignment] - self.on_release = on_release # type: ignore[assignment] + self.on_change = on_change + self.on_press = on_press + self.on_release = on_release self.enabled = enabled @@ -158,7 +159,7 @@ def __init__( def _programmatic_change(self) -> Iterator[float]: old_value = self.value on_change = self._on_change - self.on_change = None # type: ignore[assignment] + self.on_change = None yield old_value self._on_change = on_change diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 9be667f4f2..8eb1e41bb7 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Tuple, Union, no_type_check +from typing import Tuple, Union from toga.constants import Direction from toga.style import Pack @@ -119,11 +119,10 @@ def content(self, content: tuple[ContentT, ContentT]) -> None: tuple(w._impl if w is not None else None for w in _content), flex, ) - self._content = tuple(_content) # type: ignore[assignment] + self._content = tuple(_content) self.refresh() @Widget.app.setter - @no_type_check def app(self, app): # Invoke the superclass property setter Widget.app.fset(self, app) @@ -134,7 +133,6 @@ def app(self, app): content.app = app @Widget.window.setter - @no_type_check def window(self, window): # Invoke the superclass property setter Widget.window.fset(self, window) diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 7f0f1c53ab..51628864f2 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -59,10 +59,10 @@ def __init__( # 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 # type: ignore[assignment] + self.on_change = None self.value = value - self.on_change = on_change # type: ignore[assignment] + self.on_change = on_change self.enabled = enabled diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index d29e54290d..92582adeec 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -1,7 +1,8 @@ from __future__ import annotations import warnings -from typing import Any, Generic, Iterable, Literal, Protocol, TypeVar, Union +from collections.abc import Iterable +from typing import Any, Generic, Literal, Protocol, TypeVar, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source @@ -106,7 +107,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if on_double_click: - if on_activate: # type: ignore[unreachable] + if on_activate: raise ValueError("Cannot specify both on_double_click and on_activate") else: warnings.warn( @@ -136,17 +137,17 @@ def __init__( self._missing_value = missing_value or "" # Prime some properties that need to exist before the table is created. - self.on_select = None # type: ignore[assignment] - self.on_activate = None # type: ignore[assignment] - self._data = None # type: ignore[assignment] + self.on_select = None + self.on_activate = None + self._data = None self._impl = self.factory.Table(interface=self) - self.data = data # type: ignore[assignment] + self.data = data - self.on_select = on_select # type: ignore[assignment] - self.on_activate = on_activate # type: ignore[assignment] + self.on_select = on_select + self.on_activate = on_activate - @property # type: ignore[override] + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? Table widgets cannot be disabled; this property will always return True; any @@ -176,14 +177,14 @@ def data(self) -> ListSource[T]: * Otherwise, the value must be an iterable, which is copied into a new ListSource. Items are converted as shown :ref:`here `. """ - return self._data # type: ignore[return-value] + return self._data @data.setter def data(self, data: SourceT | Iterable[T] | None) -> None: if data is None: self._data = ListSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): - self._data = data # type: ignore[assignment] + self._data = data else: self._data = ListSource(accessors=self._accessors, data=data) @@ -286,7 +287,7 @@ def insert_column( if self._headings is None: if accessor is None: raise ValueError("Must specify an accessor on a table without headings") - heading = None # type: ignore[assignment] + heading = None elif not accessor: accessor = to_accessor(heading) diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 53b226709e..28566148fa 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Callable, Protocol, Union +from collections.abc import Callable +from typing import Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.style import Pack @@ -128,24 +129,24 @@ def __init__( # Create a platform specific implementation of the widget self._create() - self.placeholder = placeholder # type: ignore[assignment] + self.placeholder = placeholder self.readonly = readonly # Set the actual value before on_change, because we do not want # on_change triggered by it However, we need to prime the handler # property in case it is accessed. - self.on_change = None # type: ignore[assignment] - self.on_confirm = None # type: ignore[assignment] + self.on_change = None + self.on_confirm = None # Set the list of validators before we set the initial value so that # validation is performed on the initial value - self.validators = validators # type: ignore[assignment] - self.value = value # type: ignore[assignment] + self.validators = validators + self.value = value - self.on_change = on_change # type: ignore[assignment] - self.on_confirm = on_confirm # type: ignore[assignment] - self.on_lose_focus = on_lose_focus # type: ignore[assignment] - self.on_gain_focus = on_gain_focus # type: ignore[assignment] + self.on_change = on_change + self.on_confirm = on_confirm + 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) diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index a9f3d16959..f0338dd1a4 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -57,12 +57,12 @@ def __init__( # Create a platform specific implementation of a TimeInput self._impl = self.factory.TimeInput(interface=self) - self.on_change = None # type: ignore[assignment] - self.min = min # type: ignore[assignment] - self.max = max # type: ignore[assignment] + self.on_change = None + self.min = min + self.max = max - self.value = value # type: ignore[assignment] - self.on_change = on_change # type: ignore[assignment] + self.value = value + self.on_change = on_change @property def value(self) -> datetime.time: @@ -189,7 +189,7 @@ def min_time(self, value: object) -> None: warnings.warn( "TimePicker.min_time has been renamed TimeInput.min", DeprecationWarning ) - self.min = value # type: ignore[assignment] + self.min = value @property def max_time(self) -> datetime.time: @@ -203,4 +203,4 @@ def max_time(self, value: object) -> None: warnings.warn( "TimePicker.max_time has been renamed TimeInput.max", DeprecationWarning ) - self.max = value # type: ignore[assignment] + self.max = value diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 60ab6e0078..0972e277a8 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -1,7 +1,8 @@ from __future__ import annotations import warnings -from typing import Collection, Generic, Iterable, Literal, Protocol, TypeVar, Union +from collections.abc import Collection, Iterable +from typing import Generic, Literal, Protocol, TypeVar, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import Node, Source, TreeSource @@ -107,7 +108,7 @@ def __init__( # 2023-06: Backwards compatibility ###################################################################### if on_double_click: - if on_activate: # type: ignore[unreachable] + if on_activate: raise ValueError("Cannot specify both on_double_click and on_activate") else: warnings.warn( @@ -136,17 +137,17 @@ def __init__( self._missing_value = missing_value or "" # Prime some properties that need to exist before the tree is created. - self.on_select = None # type: ignore[assignment] - self.on_activate = None # type: ignore[assignment] - self._data = None # type: ignore[assignment] + self.on_select = None + self.on_activate = None + self._data = None self._impl = self.factory.Tree(interface=self) - self.data = data # type: ignore[assignment] + self.data = data - self.on_select = on_select # type: ignore[assignment] - self.on_activate = on_activate # type: ignore[assignment] + self.on_select = on_select + self.on_activate = on_activate - @property # type: ignore[override] + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? Tree widgets cannot be disabled; this property will always return True; any @@ -176,14 +177,14 @@ def data(self) -> TreeSource[T]: * Otherwise, the value must be a dictionary or an iterable, which is copied into a new TreeSource as shown :ref:`here `. """ - return self._data # type: ignore[return-value] + return self._data @data.setter def data(self, data: SourceT | TreeSourceDataT[T] | None) -> None: if data is None: self._data = TreeSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): - self._data = data # type: ignore[assignment] + self._data = data else: self._data = TreeSource(accessors=self._accessors, data=data) @@ -269,7 +270,7 @@ def insert_column( if self._headings is None: if accessor is None: raise ValueError("Must specify an accessor on a tree without headings") - heading = None # type: ignore[assignment] + heading = None elif not accessor: accessor = to_accessor(heading) diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 97a98e463b..761ab8918e 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -1,7 +1,8 @@ from __future__ import annotations import asyncio -from typing import Callable, Protocol, Union +from collections.abc import Callable +from typing import Protocol, Union from toga.handlers import ( AsyncResult, @@ -63,13 +64,13 @@ def __init__( super().__init__(id=id, style=style) self._impl = self.factory.WebView(interface=self) - self.user_agent = user_agent # type: ignore[assignment] + self.user_agent = user_agent # Set the load handler before loading the first URL. - self.on_webview_load = on_webview_load # type: ignore[assignment] + self.on_webview_load = on_webview_load self.url = url - def _set_url(self, url: str | None, future: asyncio.Future | None) -> None: # type: ignore[type-arg] + 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://")): raise ValueError("WebView can only display http:// and https:// URLs") @@ -90,7 +91,7 @@ def url(self) -> str | None: def url(self, value: str | None) -> None: self._set_url(value, future=None) - async def load_url(self, url: str) -> asyncio.Future: # type: ignore[type-arg] + async def load_url(self, url: str) -> asyncio.Future: """Load a URL, and wait until the next :any:`on_webview_load` event. **Note:** On Android, this method will return immediately. diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 70cbf4f5d9..d749bdac01 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -2,11 +2,11 @@ import warnings from builtins import id as identifier +from collections.abc import Iterator from pathlib import Path from typing import ( TYPE_CHECKING, Any, - Iterator, Literal, Protocol, TypeVar, @@ -140,7 +140,7 @@ class Dialog(AsyncResult): def __init__(self, window: Window, on_result: DialogResultHandlerT[Any]): # TODO:PR: should DialogResultHandlerT include the "exception" arg... - super().__init__(on_result=on_result) # type:ignore[arg-type] + super().__init__(on_result=on_result) self.window = window self.app = window.app @@ -183,14 +183,14 @@ def __init__( # 2023-08: Backwards compatibility ###################################################################### if resizeable is not None: - warnings.warn( # type: ignore[unreachable] + warnings.warn( "Window.resizeable has been renamed Window.resizable", DeprecationWarning, ) resizable = resizeable if closeable is not None: - warnings.warn( # type: ignore[unreachable] + warnings.warn( "Window.closeable has been renamed Window.closable", DeprecationWarning, ) @@ -222,7 +222,7 @@ def __init__( # Add the window to the app # _app will only be None until the window is added to the app below - self._app: App = None # type: ignore[assignment] + self._app: App = None if App.app is None: raise RuntimeError("Cannot create a Window before creating an App") App.app.windows.add(self) @@ -493,9 +493,7 @@ def full_screen(self, is_full_screen: bool) -> None: # Window capabilities ###################################################################### - def as_image( - self, format: type[ImageT] = Image # type:ignore[assignment] - ) -> ImageT: + def as_image(self, format: type[ImageT] = Image) -> ImageT: """Render the current contents of the window as an image. :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also @@ -549,11 +547,7 @@ def info_dialog( """ dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) self.factory.dialogs.InfoDialog(dialog, title, message) return dialog @@ -580,11 +574,7 @@ def question_dialog( """ dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) self.factory.dialogs.QuestionDialog(dialog, title, message) return dialog @@ -612,11 +602,7 @@ def confirm_dialog( """ dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) self.factory.dialogs.ConfirmDialog(dialog, title, message) return dialog @@ -643,11 +629,7 @@ def error_dialog( """ dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) self.factory.dialogs.ErrorDialog(dialog, title, message) return dialog @@ -712,11 +694,7 @@ def stack_trace_dialog( """ dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) self.factory.dialogs.StackTraceDialog( dialog, @@ -753,11 +731,7 @@ def save_file_dialog( """ dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) # Convert suggested filename to a path (if it isn't already), # and break it into a filename and a directory @@ -858,7 +832,7 @@ def open_file_dialog( # 2023-08: Backwards compatibility ###################################################################### if multiselect is not None: - warnings.warn( # type: ignore[unreachable] + warnings.warn( "open_file_dialog(multiselect) has been renamed multiple_select", DeprecationWarning, ) @@ -869,11 +843,7 @@ def open_file_dialog( dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) self.factory.dialogs.OpenFileDialog( dialog, @@ -961,7 +931,7 @@ def select_folder_dialog( # 2023-08: Backwards compatibility ###################################################################### if multiselect is not None: - warnings.warn( # type: ignore[unreachable] + warnings.warn( "select_folder_dialog(multiselect) has been renamed multiple_select", DeprecationWarning, ) @@ -972,11 +942,7 @@ def select_folder_dialog( dialog = Dialog( self, - on_result=( - wrapped_handler(self, on_result) - if on_result - else None # type:ignore[arg-type] # TODO:PR:revisit - ), + on_result=wrapped_handler(self, on_result) if on_result else None, ) self.factory.dialogs.SelectFolderDialog( dialog, diff --git a/tox.ini b/tox.ini index ec9fc1a3a7..da319af80e 100644 --- a/tox.ini +++ b/tox.ini @@ -26,15 +26,6 @@ deps = {tox_root}{/}core[dev] commands = pre-commit run --all-files --show-diff-on-failure --color=always -[testenv:types] -skip_install = True -changedir = core -passenv = FORCE_COLOR -deps = - {tox_root}{/}core[dev] -commands = - mypy --color-output --config ../pyproject.toml src/ - # The leading comma generates the "py" environment [testenv:py{,38,39,310,311,312,313}{,-cov}] depends = pre-commit From aaabb6ebb6a7eeb0068055163e468cd78512fd36 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 3 Jun 2024 16:27:01 -0400 Subject: [PATCH 03/13] Fixup after rebase --- .github/workflows/ci.yml | 12 -------- core/src/toga/app.py | 9 +++--- core/src/toga/hardware/location.py | 16 ++++++---- core/src/toga/icons.py | 5 ++-- core/src/toga/images.py | 3 +- core/src/toga/sources/list_source.py | 4 +-- core/src/toga/types.py | 7 ++--- core/src/toga/widgets/activityindicator.py | 6 ++-- core/src/toga/widgets/base.py | 11 +++++-- core/src/toga/widgets/box.py | 10 +++---- core/src/toga/widgets/button.py | 6 ++-- core/src/toga/widgets/canvas.py | 5 ++-- core/src/toga/widgets/dateinput.py | 5 ++-- core/src/toga/widgets/detailedlist.py | 9 +++--- core/src/toga/widgets/divider.py | 5 ++-- core/src/toga/widgets/imageview.py | 10 +++---- core/src/toga/widgets/label.py | 6 ++-- core/src/toga/widgets/mapview.py | 15 +++++----- core/src/toga/widgets/multilinetextinput.py | 5 ++-- core/src/toga/widgets/numberinput.py | 5 ++-- core/src/toga/widgets/optioncontainer.py | 9 +++--- core/src/toga/widgets/progressbar.py | 6 ++-- core/src/toga/widgets/scrollcontainer.py | 5 ++-- core/src/toga/widgets/selection.py | 11 ++++--- core/src/toga/widgets/slider.py | 8 ++--- core/src/toga/widgets/splitcontainer.py | 13 ++++---- core/src/toga/widgets/switch.py | 5 ++-- core/src/toga/widgets/table.py | 11 ++++--- core/src/toga/widgets/textinput.py | 15 +++++----- core/src/toga/widgets/timeinput.py | 5 ++-- core/src/toga/widgets/webview.py | 5 ++-- pyproject.toml | 33 --------------------- 32 files changed, 109 insertions(+), 171 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a1127fbac..929fc91893 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,12 +105,6 @@ jobs: with: fetch-depth: 0 - - name: Checkout beeware/.github - uses: actions/checkout@v4.1.2 - with: - repository: beeware/.github - path: .github - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5.1.0 with: @@ -156,12 +150,6 @@ jobs: with: fetch-depth: 0 - - name: Checkout beeware/.github - uses: actions/checkout@v4.1.2 - with: - repository: beeware/.github - path: .github - - name: Set up Python ${{ env.min_python_version }} uses: actions/setup-python@v5.1.0 with: diff --git a/core/src/toga/app.py b/core/src/toga/app.py index dcdd8e47c2..2befc885ba 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -183,11 +183,7 @@ def __iter__(self) -> Iterator[Widget]: return self.values() def __repr__(self) -> str: - return ( - "{" - + ", ".join(f"{k!r}: {v!r}" for k, v in sorted(self._registry.items())) - + "}" - ) + return f"{{{', '.join(f'{k!r}: {v!r}' for k, v in sorted(self._registry.items()))}}}" def items(self) -> Iterator[tuple[str, Widget]]: return self._registry.items() @@ -329,6 +325,9 @@ class App: #: The currently running :class:`~toga.App`. Since there can only be one running #: Toga app in a process, this is available as a class property via ``toga.App.app``. app: App + _impl: Any + _camera: Camera + _location: Location def __init__( self, diff --git a/core/src/toga/hardware/location.py b/core/src/toga/hardware/location.py index 1a52edd173..15d5c8a3d0 100644 --- a/core/src/toga/hardware/location.py +++ b/core/src/toga/hardware/location.py @@ -3,7 +3,12 @@ from typing import TYPE_CHECKING, Any, Protocol import toga -from toga.handlers import AsyncResult, PermissionResult, wrapped_handler +from toga.handlers import ( + AsyncResult, + PermissionResult, + WrappedHandlerT, + wrapped_handler, +) from toga.platform import get_platform_factory if TYPE_CHECKING: @@ -30,7 +35,6 @@ def __call__( None if the altitude could not be determined. :param kwargs: Ensures compatibility with arguments added in future versions. """ - ... class Location: @@ -132,15 +136,15 @@ def request_background_permission(self) -> PermissionResult: return result @property - def on_change(self) -> OnLocationChangeHandler: + def on_change(self) -> WrappedHandlerT: """The handler to invoke when an update to the user's location is available.""" return self._on_change @on_change.setter - def on_change(self, handler): + def on_change(self, handler: OnLocationChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) - def start_tracking(self): + def start_tracking(self) -> None: """Start monitoring the user's location for changes. An :any:`on_change` callback will be generated when the user's location @@ -156,7 +160,7 @@ def start_tracking(self): "App does not have permission to use location services" ) - def stop_tracking(self): + def stop_tracking(self) -> None: """Stop monitoring the user's location. :raises PermissionError: If the app has not requested and received permission to diff --git a/core/src/toga/icons.py b/core/src/toga/icons.py index 4cfd0f5d8e..c64177cf02 100644 --- a/core/src/toga/icons.py +++ b/core/src/toga/icons.py @@ -7,9 +7,10 @@ import toga from toga.platform import get_platform_factory -from toga.types import TypeAlias if TYPE_CHECKING: + from toga.types import TypeAlias + IconContent: TypeAlias = str | Path | toga.Icon @@ -177,5 +178,5 @@ def _full_path( raise FileNotFoundError(f"Can't find icon {self.path}") - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return isinstance(other, Icon) and other._impl.path == self._impl.path diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 1b8f24c65e..35c78c1551 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -10,7 +10,6 @@ import toga from toga.platform import get_platform_factory -from toga.types import TypeAlias, TypeVar if sys.version_info >= (3, 10): from importlib.metadata import entry_points @@ -23,6 +22,8 @@ warnings.filterwarnings("default", category=DeprecationWarning) if TYPE_CHECKING: + from toga.types import TypeAlias, TypeVar + # Define a type variable for generics where an Image type is required. ImageT = TypeVar("ImageT") diff --git a/core/src/toga/sources/list_source.py b/core/src/toga/sources/list_source.py index 2119992aae..4212a7d359 100644 --- a/core/src/toga/sources/list_source.py +++ b/core/src/toga/sources/list_source.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Collection, Iterable from typing import Generic, TypeVar from .base import Source @@ -95,7 +95,7 @@ def __delattr__(self, attr: str) -> None: # TODO:PR: consider adding supported Protocols...maybe List? class ListSource(Source, Generic[T]): - def __init__(self, accessors: Iterable[str], data: Iterable[T] | None = None): + def __init__(self, accessors: Iterable[str], data: Collection[T] | None = None): """A data source to store an ordered list of multiple data values. :param accessors: A list of attribute names for accessing the value diff --git a/core/src/toga/types.py b/core/src/toga/types.py index 3fd6da367a..dc9291be25 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -4,12 +4,9 @@ from typing import NamedTuple if sys.version_info < (3, 10): - from typing_extensions import ( # noqa:F401 - TypeAlias as TypeAlias, - TypeVar as TypeVar, - ) + from typing_extensions import TypeAlias, TypeVar # noqa:F401 else: - from typing import TypeAlias as TypeAlias, TypeVar as TypeVar # noqa:F401 + from typing import TypeAlias, TypeVar # noqa:F401 class LatLng(NamedTuple): diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index 84e1b954b3..102f44d129 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -2,16 +2,14 @@ from typing import Literal -from toga.style import Pack - -from .base import Widget +from .base import StyleT, Widget class ActivityIndicator(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, running: bool = False, ): """Create a new ActivityIndicator widget. diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 0cd99bd6f8..8a545916d4 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -1,8 +1,9 @@ from __future__ import annotations from builtins import id as identifier -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar +from travertino.declaration import BaseStyle from travertino.node import Node from toga.platform import get_platform_factory @@ -12,12 +13,18 @@ from toga.app import App from toga.window import Window +StyleT = TypeVar("StyleT", bound=BaseStyle) + class Widget(Node): _MIN_WIDTH = 100 _MIN_HEIGHT = 100 - def __init__(self, id: str | None = None, style: Pack | None = None): + def __init__( + self, + id: str | None = None, + style: StyleT | None = None, + ): """Create a base Toga widget. This is an abstract base class; it cannot be instantiated. diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index a7c71e056a..ab940e3a31 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -1,10 +1,8 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Collection -from toga.style import Pack - -from .base import Widget +from .base import StyleT, Widget class Box(Widget): @@ -14,8 +12,8 @@ class Box(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, - children: Iterable[Widget] | None = None, + style: StyleT | None = None, + children: Collection[Widget] | None = None, ): """Create a new Box container widget. diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index 5d5a2b0f23..de7ea79a36 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -4,10 +4,9 @@ import toga from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget if TYPE_CHECKING: from toga.icons import IconContent @@ -44,7 +43,7 @@ def __init__( text: str | None = None, icon: IconContent | None = None, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, on_press: OnPressHandlerT | None = None, enabled: bool = True, ): @@ -102,6 +101,7 @@ def text(self) -> str: @text.setter def text(self, value: str | None) -> None: + # \u200B: zero-width space if value is None or value == "\u200B": value = "" else: diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 1c5c1f5f54..9c7823c165 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -25,9 +25,8 @@ Font, ) from toga.handlers import WrappedHandlerT, wrapped_handler -from toga.style import Pack -from .base import Widget +from .base import StyleT, Widget if TYPE_CHECKING: from toga.images import ImageT @@ -1209,7 +1208,7 @@ class Canvas(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, on_resize: OnResizeHandler | None = None, on_press: OnTouchHandler | None = None, on_activate: OnTouchHandler | None = None, diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 49e16feafe..04a08d7939 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -5,10 +5,9 @@ from typing import Any, Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget # This accommodates the ranges of all existing implementations: # * datetime.date: 1 - 9999 @@ -46,7 +45,7 @@ class DateInput(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, value: datetime.date | None = None, min: datetime.date | None = None, max: datetime.date | None = None, diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 8a976e5013..118c1e5abe 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from collections.abc import Iterable +from collections.abc import Collection, Iterable from typing import ( Any, Generic, @@ -13,10 +13,9 @@ from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget T = TypeVar("T") SourceT = TypeVar("SourceT", bound=Source) @@ -116,7 +115,7 @@ class DetailedList(Widget, Generic[T]): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, data: SourceT | Iterable[T] | None = None, accessors: tuple[str, str, str] = ("title", "subtitle", "icon"), missing_value: str = "", @@ -215,7 +214,7 @@ def data(self) -> SourceT | ListSource[T]: return self._data @data.setter - def data(self, data: SourceT | Iterable[T] | None) -> None: + def data(self, data: SourceT | Collection[T] | None) -> None: if data is None: self._data = ListSource(data=[], accessors=self.accessors) elif isinstance(data, Source): diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index adec0acd47..4b7edf62c1 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -3,9 +3,8 @@ from typing import Literal from toga.constants import Direction -from toga.style import Pack -from .base import Widget +from .base import StyleT, Widget class Divider(Widget): @@ -15,7 +14,7 @@ class Divider(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, direction: Direction = HORIZONTAL, ): """Create a new divider line. diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index a5d4032afc..6826a3c32d 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -5,8 +5,8 @@ from travertino.size import at_least import toga -from toga.style.pack import NONE, Pack -from toga.widgets.base import Widget +from toga.style.pack import NONE +from toga.widgets.base import StyleT, Widget if TYPE_CHECKING: from toga.images import ImageContent, ImageT @@ -14,7 +14,7 @@ def rehint_imageview( image: toga.Image, - style: Pack, + style: StyleT, scale: int = 1, ) -> tuple[int, int, float | None]: """Compute the size hints for an ImageView based on the image. @@ -67,14 +67,12 @@ def rehint_imageview( return width, height, aspect_ratio -# Note: remove PIL type annotation when plugin system is implemented for image format -# registration; replace with ImageT? class ImageView(Widget): def __init__( self, image: ImageContent | None = None, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, ): """ Create a new image view. diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 4b822bd69f..83fb1ba7a6 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -1,8 +1,6 @@ from __future__ import annotations -from toga.style import Pack - -from .base import Widget +from .base import StyleT, Widget class Label(Widget): @@ -10,7 +8,7 @@ def __init__( self, text: str, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, ): """Create a new text label. diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 23425d8875..4c3661de7b 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -1,14 +1,13 @@ from __future__ import annotations -from collections.abc import Iterator +from collections.abc import Collection, Iterator from typing import Any, Protocol, Union import toga from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class MapPin: @@ -30,7 +29,7 @@ def __init__( self._subtitle = subtitle # A pin isn't tied to a map at time of creation. - self.interface: MapView = None + self.interface: MapView | None = None self._native = None def __repr__(self) -> str: @@ -65,7 +64,7 @@ def title(self, title: str) -> None: @property def subtitle(self) -> str | None: - "The subtitle of the pin." + """The subtitle of the pin.""" return self._subtitle @subtitle.setter @@ -78,7 +77,7 @@ def subtitle(self, subtitle: str | None) -> None: class MapPinSet: - def __init__(self, interface: MapView, pins: Iterator[MapPin] | None): + def __init__(self, interface: MapView, pins: Collection[MapPin] | None): self.interface = interface self._pins: set[MapPin] = set() @@ -152,10 +151,10 @@ class MapView(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, location: toga.LatLng | tuple[float, float] | None = None, zoom: int = 11, - pins: Iterator[MapPin] | None = None, + pins: Collection[MapPin] | None = None, on_select: OnSelectHandlerT | None = None, ): """Create a new MapView widget. diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 2634c57ca8..07d86cf63a 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -3,10 +3,9 @@ from typing import Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class OnChangeHandlerSync(Protocol): @@ -33,7 +32,7 @@ class MultilineTextInput(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, value: str | None = None, readonly: bool = False, placeholder: str | None = None, diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 9c09af3cda..af1b301b26 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -6,10 +6,9 @@ from typing import Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget # Implementation notes # ==================== @@ -91,7 +90,7 @@ class NumberInput(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, step: StepInputT = 1, min: NumberInputT | None = None, max: NumberInputT | None = None, diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 2941b1f5e2..43f4beb4fc 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Collection from typing import ( TYPE_CHECKING, Any, @@ -12,10 +12,9 @@ import toga from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.platform import get_platform_factory -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget if TYPE_CHECKING: from toga.icons import IconContent @@ -393,8 +392,8 @@ class OptionContainer(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, - content: Iterable[tuple[str, Widget]] | None = None, + style: StyleT | None = None, + content: Collection[tuple[str, Widget]] | None = None, on_select: OnSelectHandlerT | None = None, ): """Create a new OptionContainer. diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index be77fdf1d0..49f2e67575 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -2,9 +2,7 @@ from typing import Literal, SupportsFloat -from toga.style import Pack - -from .base import Widget +from .base import StyleT, Widget class ProgressBar(Widget): @@ -13,7 +11,7 @@ class ProgressBar(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, max: str | SupportsFloat = 1.0, value: str | SupportsFloat = 0.0, running: bool = False, diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 4409bc435b..e932edbddd 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -3,10 +3,9 @@ from typing import Literal, Protocol, SupportsInt, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class OnScrollHandlerSync(Protocol): @@ -33,7 +32,7 @@ class ScrollContainer(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, horizontal: bool = True, vertical: bool = True, on_scroll: OnScrollHandlerT | None = None, diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index c99548681e..2fb6444acc 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -1,15 +1,14 @@ from __future__ import annotations import warnings -from collections.abc import Iterable +from collections.abc import Collection from typing import Any, Generic, Protocol, TypeVar, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Source -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget T = TypeVar("T") SourceT = TypeVar("SourceT", bound=Source) @@ -39,8 +38,8 @@ class Selection(Widget, Generic[T]): def __init__( self, id: str | None = None, - style: Pack | None = None, - items: SourceT | Iterable[T] | None = None, + style: StyleT | None = None, + items: SourceT | Collection[T] | None = None, accessor: str | None = None, value: T | None = None, on_change: OnChangeHandlerT | None = None, @@ -111,7 +110,7 @@ def items(self) -> SourceT | ListSource[T]: return self._items @items.setter - def items(self, items: SourceT | Iterable[T] | None) -> None: + def items(self, items: SourceT | Collection[T] | None) -> None: if self._accessor is None: accessors = ["value"] else: diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 82b6fc5d18..6ac9ab1704 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -2,15 +2,13 @@ import warnings from abc import ABC, abstractmethod -from collections.abc import Iterator from contextlib import contextmanager from typing import Any, Protocol, SupportsFloat, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class OnChangeHandlerSync(Protocol): @@ -77,7 +75,7 @@ class Slider(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, value: float | None = None, min: float | None = None, # Default to 0.0 when range is removed max: float | None = None, # Default to 1.0 when range is removed @@ -156,7 +154,7 @@ def __init__( # Backends are inconsistent about when they produce events for programmatic changes, # so we deal with those in the interface layer. @contextmanager - def _programmatic_change(self) -> Iterator[float]: + def _programmatic_change(self) -> float: old_value = self.value on_change = self._on_change self.on_change = None diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 8eb1e41bb7..3ef8050f5a 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -2,11 +2,12 @@ from typing import Tuple, Union +from toga.app import App from toga.constants import Direction -from toga.style import Pack from toga.types import TypeAlias +from toga.window import Window -from .base import Widget +from .base import StyleT, Widget ContentT: TypeAlias = Union[Widget, Tuple[Widget, float], None] @@ -18,7 +19,7 @@ class SplitContainer(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, direction: Direction = Direction.VERTICAL, content: tuple[ContentT, ContentT] = (None, None), ): @@ -123,7 +124,7 @@ def content(self, content: tuple[ContentT, ContentT]) -> None: self.refresh() @Widget.app.setter - def app(self, app): + def app(self, app: App | None) -> None: # Invoke the superclass property setter Widget.app.fset(self, app) @@ -133,7 +134,7 @@ def app(self, app): content.app = app @Widget.window.setter - def window(self, window): + def window(self, window: Window | None) -> None: # Invoke the superclass property setter Widget.window.fset(self, window) @@ -144,7 +145,7 @@ def window(self, window): @property def direction(self) -> Direction: - """The direction of the split""" + """The direction of the split.""" return self._impl.get_direction() @direction.setter diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 51628864f2..7d3f48aee4 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -3,10 +3,9 @@ from typing import Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class OnChangeHandlerSync(Protocol): @@ -34,7 +33,7 @@ def __init__( self, text: str, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, on_change: OnChangeHandlerT | None = None, value: bool = False, enabled: bool = True, diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 92582adeec..57e02a96f4 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -1,16 +1,15 @@ from __future__ import annotations import warnings -from collections.abc import Iterable +from collections.abc import Collection, Iterable, MutableSequence from typing import Any, Generic, Literal, Protocol, TypeVar, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source from toga.sources.accessors import build_accessors, to_accessor -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget T = TypeVar("T") SourceT = TypeVar("SourceT", bound=Source) @@ -61,9 +60,9 @@ def __init__( self, headings: Iterable[str] | None = None, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, data: SourceT | Iterable[T] | None = None, - accessors: list[str] | None = None, + accessors: MutableSequence[str] | None = None, multiple_select: bool = False, on_select: OnSelectHandlerT | None = None, on_activate: OnActivateHandlerT | None = None, @@ -180,7 +179,7 @@ def data(self) -> ListSource[T]: return self._data @data.setter - def data(self, data: SourceT | Iterable[T] | None) -> None: + def data(self, data: SourceT | Collection[T] | None) -> None: if data is None: self._data = ListSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 28566148fa..ffc5c6b471 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -1,13 +1,12 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from typing import Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class OnChangeHandlerSync(Protocol): @@ -96,7 +95,7 @@ class TextInput(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, value: str | None = None, readonly: bool = False, placeholder: str | None = None, @@ -104,7 +103,7 @@ def __init__( on_confirm: OnConfirmHandlerT | None = None, on_gain_focus: OnGainFocusHandlerT | None = None, on_lose_focus: OnLoseFocusHandlerT | None = None, - validators: list[Callable[[str], bool]] | None = None, + validators: Iterable[Callable[[str], bool]] | None = None, ): """ :param id: The ID for the widget. @@ -224,12 +223,12 @@ def validators(self) -> list[Callable[[str], bool]]: return self._validators @validators.setter - def validators(self, validators: list[Callable[[str], bool]] | None) -> None: + def validators(self, validators: Iterable[Callable[[str], bool]] | None) -> None: replacing = hasattr(self, "_validators") if validators is None: self._validators = [] else: - self._validators = validators + self._validators = list(validators) if replacing: self._validate() @@ -265,7 +264,7 @@ def _validate(self) -> None: else: self._impl.clear_error() - def _value_changed(self): + def _value_changed(self) -> None: """Validate the current value of the widget and invoke the on_change handler.""" self._validate() self.on_change() diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index f0338dd1a4..1126964512 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -5,10 +5,9 @@ from typing import Any, Protocol, Union from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class OnChangeHandlerSync(Protocol): @@ -35,7 +34,7 @@ class TimeInput(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, value: datetime.time | None = None, min: datetime.time | None = None, max: datetime.time | None = None, diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 761ab8918e..2d8e455f62 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -10,10 +10,9 @@ WrappedHandlerT, wrapped_handler, ) -from toga.style import Pack from toga.types import TypeAlias -from .base import Widget +from .base import StyleT, Widget class JavaScriptResult(AsyncResult): @@ -44,7 +43,7 @@ class WebView(Widget): def __init__( self, id: str | None = None, - style: Pack | None = None, + style: StyleT | None = None, url: str | None = None, user_agent: str | None = None, on_webview_load: OnWebViewLoadHandlerT | None = None, diff --git a/pyproject.toml b/pyproject.toml index 6a4df01479..d721450568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,36 +72,3 @@ type = [ # We're not doing anything Python-related at the root level of the repo, but if this # declaration isn't here, tox commands run from the root directory raise a warning that # pyproject.toml doesn't contain a setuptools_scm section. - -[tool.mypy] -python_version = "3.8" -strict = true -show_error_context = true -warn_unused_ignores = true -warn_redundant_casts = true -warn_unreachable = true -disallow_any_unimported = true -warn_return_any = false # disable from strict=true -#disallow_any_explicit = true -#disallow_any_expr = true -#disallow_any_decorated = true - -# Specific overrides -[[tool.mypy.overrides]] -module = [ - "toga.style.*", - "toga.fonts", - "toga.widgets.base", - "toga.widgets.canvas", -] -disallow_subclassing_any = false -disallow_any_unimported = false - -# Ignore missing types for imports -[[tool.mypy.overrides]] -module = [ - "travertino.*", - "setuptools_scm", - "importlib_metadata", # alternatively, this could be installed always with [dev] -] -ignore_missing_imports = true From 411273fb822883885ed78be0a68b8a378d6d27f9 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 3 Jun 2024 18:00:53 -0400 Subject: [PATCH 04/13] Restore sync-only handler protocols --- core/src/toga/app.py | 65 ++++------ core/src/toga/command.py | 32 ++--- core/src/toga/widgets/button.py | 31 ++--- core/src/toga/widgets/dateinput.py | 34 ++---- core/src/toga/widgets/detailedlist.py | 113 +++++------------- core/src/toga/widgets/mapview.py | 31 ++--- core/src/toga/widgets/multilinetextinput.py | 34 ++---- core/src/toga/widgets/numberinput.py | 31 ++--- core/src/toga/widgets/optioncontainer.py | 29 ++--- core/src/toga/widgets/scrollcontainer.py | 31 ++--- core/src/toga/widgets/selection.py | 34 ++---- core/src/toga/widgets/slider.py | 84 ++++--------- core/src/toga/widgets/switch.py | 32 ++--- core/src/toga/widgets/table.py | 60 +++------- core/src/toga/widgets/textinput.py | 110 +++++------------ core/src/toga/widgets/timeinput.py | 32 ++--- core/src/toga/widgets/tree.py | 60 +++------- core/src/toga/widgets/webview.py | 30 ++--- core/src/toga/window.py | 110 ++++++----------- docs/reference/api/app.rst | 8 +- .../api/containers/optioncontainer.rst | 4 +- .../api/containers/scrollcontainer.rst | 4 +- docs/reference/api/resources/command.rst | 4 +- docs/reference/api/widgets/button.rst | 4 +- docs/reference/api/widgets/dateinput.rst | 4 +- docs/reference/api/widgets/detailedlist.rst | 16 +-- docs/reference/api/widgets/mapview.rst | 5 +- .../api/widgets/multilinetextinput.rst | 4 +- docs/reference/api/widgets/numberinput.rst | 4 +- docs/reference/api/widgets/slider.rst | 12 +- docs/reference/api/widgets/switch.rst | 4 +- docs/reference/api/widgets/table.rst | 8 +- docs/reference/api/widgets/textinput.rst | 16 +-- docs/reference/api/widgets/timeinput.rst | 4 +- docs/reference/api/widgets/tree.rst | 8 +- docs/reference/api/widgets/webview.rst | 4 +- docs/reference/api/window.rst | 8 +- 37 files changed, 313 insertions(+), 791 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 2befc885ba..657e2c89fd 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -9,22 +9,21 @@ from collections.abc import Iterator from email.message import Message from pathlib import Path -from typing import TYPE_CHECKING, AbstractSet, Any, MutableSet, Protocol, Union +from typing import TYPE_CHECKING, AbstractSet, Any, MutableSet, Protocol from warnings import warn from weakref import WeakValueDictionary from toga.command import CommandSet from toga.documents import Document -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.hardware.camera import Camera from toga.hardware.location import Location from toga.icons import Icon from toga.paths import Paths from toga.platform import get_platform_factory from toga.screens import Screen -from toga.types import TypeAlias from toga.widgets.base import Widget -from toga.window import OnCloseHandlerT, Window +from toga.window import OnCloseHandler, Window if TYPE_CHECKING: from toga.icons import IconContent @@ -34,67 +33,43 @@ class AppStartupMethod(Protocol): - def __call__(self, app: App, /) -> Widget: + def __call__(self, app: App, **kwargs: Any) -> Widget: """The startup method of the app. Called during app startup to set the initial main window content. :param app: The app instance that is starting. + :param kwargs: Ensures compatibility with additional arguments introduced in + future versions. :returns: The widget to use as the main window content. """ -class OnExitHandlerSync(Protocol): - def __call__(self, app: App, /) -> bool: +class OnExitHandler(Protocol): + def __call__(self, app: App, **kwargs: Any) -> bool: """A handler to invoke when the app is about to exit. The return value of this callback controls whether the app is allowed to exit. This can be used to prevent the app exiting with unsaved changes, etc. :param app: The app instance that is exiting. + :param kwargs: Ensures compatibility with additional arguments introduced in + future versions. :returns: ``True`` if the app is allowed to exit; ``False`` if the app is not allowed to exit. """ -class OnExitHandlerAsync(Protocol): - async def __call__(self, app: App, /) -> bool: - """Async definition of :any:`OnExitHandlerSync`.""" - - -class OnExitHandlerGenerator(Protocol): - def __call__(self, app: App, /) -> HandlerGeneratorReturnT[bool]: - """Generator definition of :any:`OnExitHandlerSync`.""" - - -OnExitHandlerT: TypeAlias = Union[ - OnExitHandlerSync, OnExitHandlerAsync, OnExitHandlerGenerator -] - - -class BackgroundTaskSync(Protocol): - def __call__(self, app: App, /) -> object: - """Code that will be executed as a background task. +class BackgroundTask(Protocol): + def __call__(self, app: App, **kwargs: Any) -> object: + """Code that should be executed as a background task. :param app: The app that is handling the background task. + :param kwargs: Ensures compatibility with additional arguments introduced in + future versions. """ -class BackgroundTaskAsync(Protocol): - async def __call__(self, app: App, /) -> object: - """Async definition of :any:`BackgroundTaskSync`.""" - - -class BackgroundTaskGenerator(Protocol): - def __call__(self, app: App, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`BackgroundTaskSync`.""" - - -BackgroundTaskT: TypeAlias = Union[ - BackgroundTaskSync, BackgroundTaskAsync, BackgroundTaskGenerator -] - - class WindowSet(MutableSet[Window]): def __init__(self, app: App): """A collection of windows managed by an app. @@ -270,7 +245,7 @@ def on_close(self) -> None: return None @on_close.setter - def on_close(self, handler: OnCloseHandlerT | None) -> None: + def on_close(self, handler: OnCloseHandler | None) -> None: if handler: raise ValueError( "Cannot set on_close handler for the main window. " @@ -341,7 +316,7 @@ def __init__( home_page: str | None = None, description: str | None = None, startup: AppStartupMethod | None = None, - on_exit: OnExitHandlerT | None = None, + on_exit: OnExitHandler | None = None, id: None = None, # DEPRECATED windows: None = None, # DEPRECATED ): @@ -600,7 +575,7 @@ def is_bundled(self) -> bool: # App lifecycle ###################################################################### - def add_background_task(self, handler: BackgroundTaskT) -> None: + def add_background_task(self, handler: BackgroundTask) -> None: """Schedule a task to run in the background. Schedules a coroutine or a generator to run in the background. Control @@ -837,7 +812,7 @@ def on_exit(self) -> WrappedHandlerT: return self._on_exit @on_exit.setter - def on_exit(self, handler: OnExitHandlerT | None) -> None: + def on_exit(self, handler: OnExitHandler | None) -> None: def cleanup(app: App, should_exit: bool) -> None: if should_exit or handler is None: app.exit() @@ -883,7 +858,7 @@ def __init__( description: str | None = None, startup: AppStartupMethod | None = None, document_types: dict[str, type[Document]] | None = None, - on_exit: OnExitHandlerT | None = None, + on_exit: OnExitHandler | None = None, id: None = None, # DEPRECATED ): """Create a document-based application. diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 8ceeacf4ad..7756fe3f04 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -1,13 +1,12 @@ from __future__ import annotations from collections.abc import Iterator -from typing import TYPE_CHECKING, Protocol, Union +from typing import TYPE_CHECKING, Protocol -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.icons import Icon from toga.keys import Key from toga.platform import get_platform_factory -from toga.types import TypeAlias if TYPE_CHECKING: from toga.app import App @@ -150,35 +149,20 @@ def key(self) -> tuple[tuple[int, int, str], ...]: Group.HELP = Group("Help", order=100) -class ActionHandlerSync(Protocol): - def __call__(self, command: Command, /) -> bool: +class ActionHandler(Protocol): + def __call__(self, command: Command, **kwargs) -> bool: """A handler that will be invoked when a Command is invoked. :param command: The command that triggered the action. + :param kwargs: Ensures compatibility with additional arguments introduced in + future versions. """ -class ActionHandlerAsync(Protocol): - async def __call__(self, command: Command, /) -> bool: - """Async definition of :any:`ActionHandlerSync`.""" - - -class ActionHandlerGenerator(Protocol): - async def __call__(self, command: Command, /) -> HandlerGeneratorReturnT[bool]: - """Generator definition of :any:`ActionHandlerSync`.""" - - -ActionHandlerT: TypeAlias = Union[ - ActionHandlerSync, - ActionHandlerAsync, - ActionHandlerGenerator, -] - - class Command: def __init__( self, - action: ActionHandlerT | None, + action: ActionHandler | None, text: str, *, shortcut: str | Key | None = None, @@ -267,7 +251,7 @@ def action(self) -> WrappedHandlerT | None: return self._action @action.setter - def action(self, action: ActionHandlerT | None) -> None: + def action(self, action: ActionHandler | None) -> None: """Set the action attached to the command Needs to be a valid ActionHandler or ``None`` diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index de7ea79a36..14b8bb5433 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -1,10 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Protocol, Union +from typing import TYPE_CHECKING, Any, Protocol import toga -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget @@ -12,31 +11,15 @@ from toga.icons import IconContent -class OnPressHandlerSync(Protocol): - def __call__(self, widget: Button, /) -> object: +class OnPressHandler(Protocol): + def __call__(self, widget: Button, **kwargs: Any) -> object: """A handler that will be invoked when a button is pressed. :param widget: The button that was pressed. + :param kwargs: Ensures compatibility with arguments added in future versions. """ -class OnPressHandlerAsync(Protocol): - async def __call__(self, widget: Button, /) -> object: - """Async definition of :any:`OnPressHandlerSync`.""" - - -class OnPressHandlerGenerator(Protocol): - def __call__(self, widget: Button, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnPressHandlerSync`.""" - - -OnPressHandlerT: TypeAlias = Union[ - OnPressHandlerSync, - OnPressHandlerAsync, - OnPressHandlerGenerator, -] - - class Button(Widget): def __init__( self, @@ -44,7 +27,7 @@ def __init__( icon: IconContent | None = None, id: str | None = None, style: StyleT | None = None, - on_press: OnPressHandlerT | None = None, + on_press: toga.widgets.button.OnPressHandler | None = None, enabled: bool = True, ): """Create a new button widget. @@ -155,5 +138,5 @@ def on_press(self) -> WrappedHandlerT: return self._on_press @on_press.setter - def on_press(self, handler: OnPressHandlerT) -> None: + def on_press(self, handler: toga.widgets.button.OnPressHandler) -> None: self._on_press = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 04a08d7939..1dbb305812 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -2,10 +2,10 @@ import datetime import warnings -from typing import Any, Protocol, Union +from typing import Any, Protocol -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +import toga +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget @@ -17,26 +17,12 @@ MAX_DATE = datetime.date(8999, 12, 31) -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler that will be invoked when the value changes.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> None: + """A handler that will be invoked when a change occurs. - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, - OnChangeHandlerAsync, - OnChangeHandlerGenerator, -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class DateInput(Widget): @@ -49,7 +35,7 @@ def __init__( value: datetime.date | None = None, min: datetime.date | None = None, max: datetime.date | None = None, - on_change: OnChangeHandlerT | None = None, + on_change: toga.widgets.dateinput.OnChangeHandler | None = None, ): """Create a new DateInput widget. @@ -173,7 +159,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change(self, handler: toga.widgets.dateinput.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 118c1e5abe..752f17cc0c 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -8,12 +8,11 @@ Literal, Protocol, TypeVar, - Union, ) -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +import toga.widgets.detailedlist +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source -from toga.types import TypeAlias from .base import StyleT, Widget @@ -21,94 +20,38 @@ SourceT = TypeVar("SourceT", bound=Source) -class OnPrimaryActionHandlerSync(Protocol): - def __call__(self, row: Any, /) -> object: +class OnPrimaryActionHandler(Protocol): + def __call__(self, row: Any, **kwargs: Any) -> object: """A handler to invoke for the primary action. - :param row: The current row for the detailed list. + :param widget: The button that was pressed. + :param kwargs: Ensures compatibility with arguments added in future versions. """ -class OnPrimaryActionHandlerAsync(Protocol): - async def __call__(self, row: Any, /) -> object: - """Async definition of :any:`OnPrimaryActionHandlerSync`.""" - - -class OnPrimaryActionHandlerGenerator(Protocol): - def __call__(self, row: Any, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnPrimaryActionHandlerSync`.""" - - -OnPrimaryActionHandlerT: TypeAlias = Union[ - OnPrimaryActionHandlerSync, - OnPrimaryActionHandlerAsync, - OnPrimaryActionHandlerGenerator, -] - - -class OnSecondaryActionHandlerSync(Protocol): - def __call__(self, row: Any, /) -> object: +class OnSecondaryActionHandler(Protocol): + def __call__(self, row: Any, **kwargs: Any) -> object: """A handler to invoke for the secondary action. :param row: The current row for the detailed list. + :param kwargs: Ensures compatibility with arguments added in future versions. """ -class OnSecondaryActionHandlerAsync(Protocol): - async def __call__(self, row: Any, /) -> object: - """Async definition of :any:`OnSecondaryActionHandlerSync`.""" - - -class OnSecondaryActionHandlerGenerator(Protocol): - def __call__(self, row: Any, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnSecondaryActionHandlerSync`.""" - - -OnSecondaryActionHandlerT: TypeAlias = Union[ - OnSecondaryActionHandlerSync, - OnSecondaryActionHandlerAsync, - OnSecondaryActionHandlerGenerator, -] - - -class OnRefreshHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the detailed list is refreshed.""" - - -class OnRefreshHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnRefreshHandlerSync`.""" - - -class OnRefreshHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnRefreshHandlerSync`.""" - - -OnRefreshHandlerT: TypeAlias = Union[ - OnRefreshHandlerSync, OnRefreshHandlerAsync, OnRefreshHandlerGenerator -] - - -class OnSelectHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the detailed list is selected.""" - - -class OnSelectHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnSelectHandlerSync`.""" +class OnRefreshHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the detailed list is refreshed. + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -class OnSelectHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnSelectHandlerSync`.""" +class OnSelectHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the detailed list is selected. -OnSelectHandlerT: TypeAlias = Union[ - OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class DetailedList(Widget, Generic[T]): @@ -120,11 +63,11 @@ def __init__( accessors: tuple[str, str, str] = ("title", "subtitle", "icon"), missing_value: str = "", primary_action: str | None = "Delete", - on_primary_action: OnPrimaryActionHandlerT | None = None, + on_primary_action: OnPrimaryActionHandler | None = None, secondary_action: str | None = "Action", - on_secondary_action: OnSecondaryActionHandlerT | None = None, - on_refresh: OnRefreshHandlerT | None = None, - on_select: OnSelectHandlerT | None = None, + on_secondary_action: OnSecondaryActionHandler | None = None, + on_refresh: OnRefreshHandler | None = None, + on_select: toga.widgets.detailedlist.OnSelectHandler | None = None, on_delete: None = None, # DEPRECATED ): """Create a new DetailedList widget. @@ -282,7 +225,7 @@ def on_primary_action(self) -> WrappedHandlerT: return self._on_primary_action @on_primary_action.setter - def on_primary_action(self, handler: OnPrimaryActionHandlerT) -> None: + def on_primary_action(self, handler: OnPrimaryActionHandler) -> None: self._on_primary_action = wrapped_handler(self, handler) self._impl.set_primary_action_enabled(handler is not None) @@ -300,7 +243,7 @@ def on_secondary_action(self) -> WrappedHandlerT: return self._on_secondary_action @on_secondary_action.setter - def on_secondary_action(self, handler: OnSecondaryActionHandlerT) -> None: + def on_secondary_action(self, handler: OnSecondaryActionHandler) -> None: self._on_secondary_action = wrapped_handler(self, handler) self._impl.set_secondary_action_enabled(handler is not None) @@ -315,7 +258,7 @@ def on_refresh(self) -> WrappedHandlerT: return self._on_refresh @on_refresh.setter - def on_refresh(self, handler: OnRefreshHandlerT) -> None: + def on_refresh(self, handler: OnRefreshHandler) -> None: self._on_refresh = wrapped_handler( self, handler, cleanup=self._impl.after_on_refresh ) @@ -327,7 +270,7 @@ def on_select(self) -> WrappedHandlerT: return self._on_select @on_select.setter - def on_select(self, handler: OnSelectHandlerT) -> None: + def on_select(self, handler: toga.widgets.detailedlist.OnSelectHandler) -> None: self._on_select = wrapped_handler(self, handler) ###################################################################### @@ -344,7 +287,7 @@ def on_delete(self) -> WrappedHandlerT: return self.on_primary_action @on_delete.setter - def on_delete(self, handler: OnPrimaryActionHandlerT) -> None: + def on_delete(self, handler: OnPrimaryActionHandler) -> None: warnings.warn( "DetailedList.on_delete has been renamed DetailedList.on_primary_action.", DeprecationWarning, diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 4c3661de7b..3c753ca487 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -1,11 +1,10 @@ from __future__ import annotations from collections.abc import Collection, Iterator -from typing import Any, Protocol, Union +from typing import Any, Protocol import toga -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget @@ -121,32 +120,16 @@ def clear(self) -> None: self._pins = set() -class OnSelectHandlerSync(Protocol): - def __call__(self, widget: MapView, /, *, pin: MapPin) -> object: +class OnSelectHandler(Protocol): + def __call__(self, widget: MapView, *, pin: MapPin, **kwargs: Any) -> object: """A handler that will be invoked when the user selects a map pin. :param widget: The button that was pressed. :param pin: The pin that was selected. + :param kwargs: Ensures compatibility with arguments added in future versions. """ -class OnSelectHandlerAsync(Protocol): - async def __call__(self, widget: MapView, /, *, pin: MapPin) -> object: - """Async definition of :any:`OnSelectHandlerSync`.""" - - -class OnSelectHandlerGenerator(Protocol): - def __call__( - self, widget: MapView, /, *, pin: MapPin - ) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnSelectHandlerSync`.""" - - -OnSelectHandlerT: TypeAlias = Union[ - OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator -] - - class MapView(Widget): def __init__( self, @@ -155,7 +138,7 @@ def __init__( location: toga.LatLng | tuple[float, float] | None = None, zoom: int = 11, pins: Collection[MapPin] | None = None, - on_select: OnSelectHandlerT | None = None, + on_select: toga.widgets.mapview.OnSelectHandler | None = None, ): """Create a new MapView widget. @@ -258,7 +241,7 @@ def on_select(self) -> WrappedHandlerT: return self._on_select @on_select.setter - def on_select(self, handler: OnSelectHandlerT | None) -> None: + def on_select(self, handler: toga.widgets.mapview.OnSelectHandler | None) -> None: if handler and not getattr(self._impl, "SUPPORTS_ON_SELECT", True): self.factory.not_implemented("MapView.on_select") diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 07d86cf63a..c54d4cc8b6 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -1,31 +1,19 @@ from __future__ import annotations -from typing import Protocol, Union +from typing import Any, Protocol -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +import toga.widgets.multilinetextinput +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the value is changed.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the value is changed. - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class MultilineTextInput(Widget): @@ -36,7 +24,7 @@ def __init__( value: str | None = None, readonly: bool = False, placeholder: str | None = None, - on_change: OnChangeHandlerT | None = None, + on_change: toga.widgets.multilinetextinput.OnChangeHandler | None = None, ): """Create a new multi-line text input widget. @@ -122,5 +110,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change( + self, handler: toga.widgets.multilinetextinput.OnChangeHandler + ) -> None: self._on_change = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index af1b301b26..0cf3f50305 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -3,9 +3,10 @@ import re import warnings from decimal import ROUND_HALF_UP, Decimal, InvalidOperation -from typing import Protocol, Union +from typing import Any, Protocol, Union -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +import toga.widgets.numberinput +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.types import TypeAlias from .base import StyleT, Widget @@ -66,24 +67,12 @@ def _clean_decimal_str(value: str) -> str: return value -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the value is changed.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the value is changed. - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class NumberInput(Widget): @@ -96,7 +85,7 @@ def __init__( max: NumberInputT | None = None, value: NumberInputT | None = None, readonly: bool = False, - on_change: OnChangeHandlerT | None = None, + on_change: toga.widgets.numberinput.OnChangeHandler | None = None, min_value: None = None, # DEPRECATED max_value: None = None, # DEPRECATED ): @@ -311,7 +300,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change(self, handler: toga.widgets.numberinput.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) ###################################################################### diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 43f4beb4fc..c40acfb60c 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -5,12 +5,11 @@ TYPE_CHECKING, Any, Protocol, - Union, overload, ) import toga -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.platform import get_platform_factory from toga.types import TypeAlias @@ -27,24 +26,12 @@ ) -class OnSelectHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the option list is selected.""" +class OnSelectHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the option list is selected. - -class OnSelectHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnSelectHandlerSync`.""" - - -class OnSelectHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnSelectHandlerSync`.""" - - -OnSelectHandlerT: TypeAlias = Union[ - OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class OptionItem: @@ -394,7 +381,7 @@ def __init__( id: str | None = None, style: StyleT | None = None, content: Collection[tuple[str, Widget]] | None = None, - on_select: OnSelectHandlerT | None = None, + on_select: toga.widgets.optioncontainer.OnSelectHandler | None = None, ): """Create a new OptionContainer. @@ -501,5 +488,5 @@ def on_select(self) -> WrappedHandlerT: return self._on_select @on_select.setter - def on_select(self, handler: OnSelectHandlerT) -> None: + def on_select(self, handler: toga.widgets.optioncontainer.OnSelectHandler) -> None: self._on_select = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index e932edbddd..4b3e43aad9 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -1,31 +1,18 @@ from __future__ import annotations -from typing import Literal, Protocol, SupportsInt, Union +from typing import Any, Literal, Protocol, SupportsInt -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget -class OnScrollHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the container is scrolled.""" +class OnScrollHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the container is scrolled. - -class OnScrollHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnScrollHandlerSync`.""" - - -class OnScrollHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnScrollHandlerSync`.""" - - -OnScrollHandlerT: TypeAlias = Union[ - OnScrollHandlerSync, OnScrollHandlerAsync, OnScrollHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class ScrollContainer(Widget): @@ -35,7 +22,7 @@ def __init__( style: StyleT | None = None, horizontal: bool = True, vertical: bool = True, - on_scroll: OnScrollHandlerT | None = None, + on_scroll: OnScrollHandler | None = None, content: Widget | None = None, ): """Create a new Scroll Container. @@ -148,7 +135,7 @@ def on_scroll(self) -> WrappedHandlerT: return self._on_scroll @on_scroll.setter - def on_scroll(self, on_scroll: OnScrollHandlerT) -> None: + def on_scroll(self, on_scroll: OnScrollHandler) -> None: self._on_scroll = wrapped_handler(self, on_scroll) @property diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 2fb6444acc..a653b60ea0 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -2,11 +2,11 @@ import warnings from collections.abc import Collection -from typing import Any, Generic, Protocol, TypeVar, Union +from typing import Any, Generic, Protocol, TypeVar -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +import toga.widgets.selection +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Source -from toga.types import TypeAlias from .base import StyleT, Widget @@ -14,24 +14,12 @@ SourceT = TypeVar("SourceT", bound=Source) -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the value is changed.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the value is changed. - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class Selection(Widget, Generic[T]): @@ -42,7 +30,7 @@ def __init__( items: SourceT | Collection[T] | None = None, accessor: str | None = None, value: T | None = None, - on_change: OnChangeHandlerT | None = None, + on_change: toga.widgets.selection.OnChangeHandler | None = None, enabled: bool = True, on_select: None = None, # DEPRECATED ): @@ -199,7 +187,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change(self, handler: toga.widgets.selection.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) ###################################################################### @@ -216,7 +204,7 @@ def on_select(self) -> WrappedHandlerT: return self.on_change @on_select.setter - def on_select(self, handler: OnChangeHandlerT) -> None: + def on_select(self, handler: toga.widgets.selection.OnChangeHandler) -> None: warnings.warn( "Selection.on_select has been renamed Selection.on_change.", DeprecationWarning, diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 6ac9ab1704..a134ca9c95 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -3,72 +3,36 @@ import warnings from abc import ABC, abstractmethod from contextlib import contextmanager -from typing import Any, Protocol, SupportsFloat, Union +from typing import Any, Protocol, SupportsFloat -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +import toga +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the value is changed.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the value is changed. - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator -] - - -class OnPressHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the slider is pressed.""" - - -class OnPressHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnPressHandlerSync`.""" - - -class OnPressHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnPressHandlerSync`.""" - - -OnPressHandlerT: TypeAlias = Union[ - OnPressHandlerSync, OnPressHandlerAsync, OnPressHandlerGenerator -] - - -class OnReleaseHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the slider is pressed.""" + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -class OnReleaseHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnReleaseHandlerSync`.""" +class OnPressHandler(Protocol): + def __call__(self, **kwargs) -> object: + """A handler to invoke when the slider is pressed. + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -class OnReleaseHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnReleaseHandlerSync`.""" +class OnReleaseHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the slider is pressed. -OnReleaseHandlerT: TypeAlias = Union[ - OnReleaseHandlerSync, OnReleaseHandlerAsync, OnReleaseHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class Slider(Widget): @@ -80,9 +44,9 @@ def __init__( min: float | None = None, # Default to 0.0 when range is removed max: float | None = None, # Default to 1.0 when range is removed tick_count: int | None = None, - on_change: OnChangeHandlerT | None = None, - on_press: OnPressHandlerT | None = None, - on_release: OnReleaseHandlerT | None = None, + on_change: toga.widgets.slider.OnChangeHandler | None = None, + on_press: toga.widgets.slider.OnPressHandler | None = None, + on_release: OnReleaseHandler | None = None, enabled: bool = True, range: None = None, # DEPRECATED ): @@ -326,7 +290,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change(self, handler: toga.widgets.slider.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) @property @@ -335,7 +299,7 @@ def on_press(self) -> WrappedHandlerT: return self._on_press @on_press.setter - def on_press(self, handler: OnPressHandlerT) -> None: + def on_press(self, handler: toga.widgets.slider.OnPressHandler) -> None: self._on_press = wrapped_handler(self, handler) @property @@ -344,7 +308,7 @@ def on_release(self) -> WrappedHandlerT: return self._on_release @on_release.setter - def on_release(self, handler: OnReleaseHandlerT) -> None: + def on_release(self, handler: OnReleaseHandler) -> None: self._on_release = wrapped_handler(self, handler) ###################################################################### diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 7d3f48aee4..b6b6fc79a2 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -1,31 +1,19 @@ from __future__ import annotations -from typing import Protocol, Union +from typing import Any, Protocol -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +import toga.widgets.switch +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the value is changed.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the value is changed. - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class Switch(Widget): @@ -34,7 +22,7 @@ def __init__( text: str, id: str | None = None, style: StyleT | None = None, - on_change: OnChangeHandlerT | None = None, + on_change: toga.widgets.switch.OnChangeHandler | None = None, value: bool = False, enabled: bool = True, ): @@ -96,7 +84,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change(self, handler: toga.widgets.switch.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) @property diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 57e02a96f4..89607ac7e9 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -2,12 +2,12 @@ import warnings from collections.abc import Collection, Iterable, MutableSequence -from typing import Any, Generic, Literal, Protocol, TypeVar, Union +from typing import Any, Generic, Literal, Protocol, TypeVar -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +import toga +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source from toga.sources.accessors import build_accessors, to_accessor -from toga.types import TypeAlias from .base import StyleT, Widget @@ -15,44 +15,20 @@ SourceT = TypeVar("SourceT", bound=Source) -class OnSelectHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the table is selected.""" - - -class OnSelectHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnSelectHandlerSync`.""" - - -class OnSelectHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnSelectHandlerSync`.""" - - -OnSelectHandlerT: TypeAlias = Union[ - OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator -] - - -class OnActivateHandlerSync(Protocol): - def __call__(self, row: Any, /) -> object: - """A handler to invoke when the table is activated.""" - - -class OnActivateHandlerAsync(Protocol): - async def __call__(self, row: Any, /) -> object: - """Async definition of :any:`OnActivateHandlerSync`.""" +class OnSelectHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the table is selected. + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -class OnActivateHandlerGenerator(Protocol): - def __call__(self, row: Any, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnActivateHandlerSync`.""" +class OnActivateHandler(Protocol): + def __call__(self, row: Any, **kwargs: Any) -> object: + """A handler to invoke when the table is activated. -OnActivateHandlerT: TypeAlias = Union[ - OnActivateHandlerSync, OnActivateHandlerAsync, OnActivateHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class Table(Widget, Generic[T]): @@ -64,8 +40,8 @@ def __init__( data: SourceT | Iterable[T] | None = None, accessors: MutableSequence[str] | None = None, multiple_select: bool = False, - on_select: OnSelectHandlerT | None = None, - on_activate: OnActivateHandlerT | None = None, + on_select: toga.widgets.table.OnSelectHandler | None = None, + on_activate: toga.widgets.table.OnActivateHandler | None = None, missing_value: str = "", on_double_click: None = None, # DEPRECATED ): @@ -240,7 +216,7 @@ def on_select(self) -> WrappedHandlerT: return self._on_select @on_select.setter - def on_select(self, handler: OnSelectHandlerT) -> None: + def on_select(self, handler: toga.widgets.table.OnSelectHandler) -> None: self._on_select = wrapped_handler(self, handler) @property @@ -250,7 +226,7 @@ def on_activate(self) -> WrappedHandlerT: return self._on_activate @on_activate.setter - def on_activate(self, handler: OnActivateHandlerT) -> None: + def on_activate(self, handler: toga.widgets.table.OnActivateHandler) -> None: self._on_activate = wrapped_handler(self, handler) def add_column(self, heading: str, accessor: str | None = None) -> None: @@ -359,7 +335,7 @@ def on_double_click(self) -> WrappedHandlerT: return self.on_activate @on_double_click.setter - def on_double_click(self, handler: OnActivateHandlerT) -> None: + def on_double_click(self, handler: toga.widgets.table.OnActivateHandler) -> None: warnings.warn( "Table.on_double_click has been renamed Table.on_activate.", DeprecationWarning, diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index ffc5c6b471..a4fa6cbf03 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -1,92 +1,44 @@ from __future__ import annotations from collections.abc import Callable, Iterable -from typing import Protocol, Union +from typing import Any, Protocol -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +import toga +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the text input is changed.""" - - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator -] - - -class OnConfirmHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the text input is confirmed.""" - - -class OnConfirmHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnConfirmHandlerSync`.""" - - -class OnConfirmHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnConfirmHandlerSync`.""" - - -OnConfirmHandlerT: TypeAlias = Union[ - OnConfirmHandlerSync, OnConfirmHandlerAsync, OnConfirmHandlerGenerator -] - - -class OnGainFocusHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the text input gains focus.""" - - -class OnGainFocusHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnGainFocusHandlerSync`.""" - - -class OnGainFocusHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnGainFocusHandlerSync`.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the text input is changed. + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -OnGainFocusHandlerT: TypeAlias = Union[ - OnGainFocusHandlerSync, OnGainFocusHandlerAsync, OnGainFocusHandlerGenerator -] +class OnConfirmHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the text input is confirmed. -class OnLoseFocusHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the text input loses focus.""" + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -class OnLoseFocusHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnLoseFocusHandlerSync`.""" +class OnGainFocusHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the text input gains focus. + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -class OnLoseFocusHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnLoseFocusHandlerSync`.""" +class OnLoseFocusHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the text input loses focus. -OnLoseFocusHandlerT: TypeAlias = Union[ - OnLoseFocusHandlerSync, OnLoseFocusHandlerAsync, OnLoseFocusHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class TextInput(Widget): @@ -99,10 +51,10 @@ def __init__( value: str | None = None, readonly: bool = False, placeholder: str | None = None, - on_change: OnChangeHandlerT | None = None, - on_confirm: OnConfirmHandlerT | None = None, - on_gain_focus: OnGainFocusHandlerT | None = None, - on_lose_focus: OnLoseFocusHandlerT | None = None, + on_change: toga.widgets.textinput.OnChangeHandler | None = None, + on_confirm: OnConfirmHandler | None = None, + on_gain_focus: OnGainFocusHandler | None = None, + on_lose_focus: OnLoseFocusHandler | None = None, validators: Iterable[Callable[[str], bool]] | None = None, ): """ @@ -211,7 +163,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change(self, handler: toga.widgets.textinput.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) @property @@ -239,7 +191,7 @@ def on_gain_focus(self) -> WrappedHandlerT: return self._on_gain_focus @on_gain_focus.setter - def on_gain_focus(self, handler: OnGainFocusHandlerT) -> None: + def on_gain_focus(self, handler: OnGainFocusHandler) -> None: self._on_gain_focus = wrapped_handler(self, handler) @property @@ -248,7 +200,7 @@ def on_lose_focus(self) -> WrappedHandlerT: return self._on_lose_focus @on_lose_focus.setter - def on_lose_focus(self, handler: OnLoseFocusHandlerT) -> None: + def on_lose_focus(self, handler: OnLoseFocusHandler) -> None: self._on_lose_focus = wrapped_handler(self, handler) def _validate(self) -> None: @@ -277,5 +229,5 @@ def on_confirm(self) -> WrappedHandlerT: return self._on_confirm @on_confirm.setter - def on_confirm(self, handler: OnConfirmHandlerT) -> None: + def on_confirm(self, handler: OnConfirmHandler) -> None: self._on_confirm = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 1126964512..9dbc44e284 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -2,32 +2,20 @@ import datetime import warnings -from typing import Any, Protocol, Union +from typing import Any, Protocol -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler -from toga.types import TypeAlias +import toga +from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget -class OnChangeHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the time input is changed.""" +class OnChangeHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the time input is changed. - -class OnChangeHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnChangeHandlerSync`.""" - - -class OnChangeHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnChangeHandlerSync`.""" - - -OnChangeHandlerT: TypeAlias = Union[ - OnChangeHandlerSync, OnChangeHandlerAsync, OnChangeHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class TimeInput(Widget): @@ -38,7 +26,7 @@ def __init__( value: datetime.time | None = None, min: datetime.time | None = None, max: datetime.time | None = None, - on_change: OnChangeHandlerT | None = None, + on_change: toga.widgets.timeinput.OnChangeHandler | None = None, ): """Create a new TimeInput widget. @@ -150,7 +138,7 @@ def on_change(self) -> WrappedHandlerT: return self._on_change @on_change.setter - def on_change(self, handler: OnChangeHandlerT) -> None: + def on_change(self, handler: toga.widgets.timeinput.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 0972e277a8..9b8617ee9f 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -2,14 +2,14 @@ import warnings from collections.abc import Collection, Iterable -from typing import Generic, Literal, Protocol, TypeVar, Union +from typing import Any, Generic, Literal, Protocol, TypeVar -from toga.handlers import HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler +import toga +from toga.handlers import WrappedHandlerT, wrapped_handler from toga.sources import Node, Source, TreeSource from toga.sources.accessors import build_accessors, to_accessor from toga.sources.tree_source import TreeSourceDataT from toga.style import Pack -from toga.types import TypeAlias from .base import Widget @@ -17,44 +17,20 @@ SourceT = TypeVar("SourceT", bound=Source) -class OnSelectHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the tree is selected.""" - - -class OnSelectHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnSelectHandlerSync`.""" - - -class OnSelectHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnSelectHandlerSync`.""" - - -OnSelectHandlerT: TypeAlias = Union[ - OnSelectHandlerSync, OnSelectHandlerAsync, OnSelectHandlerGenerator -] - - -class OnActivateHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the tree is activated.""" - - -class OnActivateHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnActivateHandlerSync`.""" +class OnSelectHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the tree is selected. + :param kwargs: Ensures compatibility with arguments added in future versions. + """ -class OnActivateHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnActivateHandlerSync`.""" +class OnActivateHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the tree is activated. -OnActivateHandlerT: TypeAlias = Union[ - OnActivateHandlerSync, OnActivateHandlerAsync, OnActivateHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class Tree(Widget, Generic[T]): @@ -66,8 +42,8 @@ def __init__( data: SourceT | TreeSourceDataT[T] | None = None, accessors: Collection[str] | None = None, multiple_select: bool = False, - on_select: OnSelectHandlerT | None = None, - on_activate: OnActivateHandlerT | None = None, + on_select: toga.widgets.tree.OnSelectHandler | None = None, + on_activate: toga.widgets.tree.OnActivateHandler | None = None, missing_value: str = "", on_double_click: None = None, # DEPRECATED ): @@ -333,7 +309,7 @@ def on_select(self) -> WrappedHandlerT: return self._on_select @on_select.setter - def on_select(self, handler: OnSelectHandlerT) -> None: + def on_select(self, handler: toga.widgets.tree.OnSelectHandler) -> None: self._on_select = wrapped_handler(self, handler) @property @@ -343,7 +319,7 @@ def on_activate(self) -> WrappedHandlerT: return self._on_activate @on_activate.setter - def on_activate(self, handler: OnActivateHandlerT) -> None: + def on_activate(self, handler: toga.widgets.tree.OnActivateHandler) -> None: self._on_activate = wrapped_handler(self, handler) ###################################################################### @@ -360,7 +336,7 @@ def on_double_click(self) -> WrappedHandlerT: return self.on_activate @on_double_click.setter - def on_double_click(self, handler: OnActivateHandlerT) -> None: + def on_double_click(self, handler: toga.widgets.tree.OnActivateHandler) -> None: warnings.warn( "Tree.on_double_click has been renamed Tree.on_activate.", DeprecationWarning, diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 2d8e455f62..b1aa4d3a0f 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -2,15 +2,13 @@ import asyncio from collections.abc import Callable -from typing import Protocol, Union +from typing import Any, Protocol from toga.handlers import ( AsyncResult, - HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler, ) -from toga.types import TypeAlias from .base import StyleT, Widget @@ -19,24 +17,12 @@ class JavaScriptResult(AsyncResult): RESULT_TYPE = "JavaScript" -class OnWebViewLoadHandlerSync(Protocol): - def __call__(self, /) -> object: - """A handler to invoke when the WebView is loaded.""" +class OnWebViewLoadHandler(Protocol): + def __call__(self, **kwargs: Any) -> object: + """A handler to invoke when the WebView is loaded. - -class OnWebViewLoadHandlerAsync(Protocol): - async def __call__(self, /) -> object: - """Async definition of :any:`OnWebViewLoadHandlerSync`.""" - - -class OnWebViewLoadHandlerGenerator(Protocol): - def __call__(self, /) -> HandlerGeneratorReturnT[object]: - """Generator definition of :any:`OnWebViewLoadHandlerSync`.""" - - -OnWebViewLoadHandlerT: TypeAlias = Union[ - OnWebViewLoadHandlerSync, OnWebViewLoadHandlerAsync, OnWebViewLoadHandlerGenerator -] + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class WebView(Widget): @@ -46,7 +32,7 @@ def __init__( style: StyleT | None = None, url: str | None = None, user_agent: str | None = None, - on_webview_load: OnWebViewLoadHandlerT | None = None, + on_webview_load: OnWebViewLoadHandler | None = None, ): """Create a new WebView widget. @@ -120,7 +106,7 @@ def on_webview_load(self) -> WrappedHandlerT: return self._on_webview_load @on_webview_load.setter - def on_webview_load(self, handler: OnWebViewLoadHandlerT) -> None: + def on_webview_load(self, handler: OnWebViewLoadHandler) -> None: if handler and not getattr(self._impl, "SUPPORTS_ON_WEBVIEW_LOAD", True): self.factory.not_implemented("WebView.on_webview_load") diff --git a/core/src/toga/window.py b/core/src/toga/window.py index d749bdac01..c2c112c4f9 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -10,20 +10,17 @@ Literal, Protocol, TypeVar, - Union, overload, ) from toga.command import CommandSet from toga.handlers import ( AsyncResult, - HandlerGeneratorReturnT, WrappedHandlerT, wrapped_handler, ) from toga.images import Image from toga.platform import get_platform_factory -from toga.types import TypeAlias if TYPE_CHECKING: from toga.app import App @@ -77,68 +74,37 @@ def values(self) -> Iterator[Widget]: yield item[1] -class OnCloseHandlerSync(Protocol): - def __call__(self, window: Window, /) -> bool: +class OnCloseHandler(Protocol): + def __call__(self, window: Window, **kwargs: Any) -> bool: """A handler to invoke when a window is about to close. The return value of this callback controls whether the window is allowed to close. This can be used to prevent a window closing with unsaved changes, etc. :param window: The window instance that is closing. + :param kwargs: Ensures compatibility with arguments added in future versions. :returns: ``True`` if the window is allowed to close; ``False`` if the window is not allowed to close. """ -class OnCloseHandlerAsync(Protocol): - async def __call__(self, window: Window, /) -> bool: - """Async definition of :any:`OnCloseHandlerSync`.""" - - -class OnCloseHandlerGenerator(Protocol): - def __call__(self, window: Window, /) -> HandlerGeneratorReturnT[bool]: - """Generator definition of :any:`OnCloseHandlerSync`.""" - - -OnCloseHandlerT: TypeAlias = Union[ - OnCloseHandlerSync, OnCloseHandlerAsync, OnCloseHandlerGenerator -] - _DialogResultT = TypeVar("_DialogResultT", contravariant=True) -class DialogResultHandlerSync(Protocol[_DialogResultT]): - def __call__(self, window: Window, result: _DialogResultT, /) -> Any: +class DialogResultHandler(Protocol[_DialogResultT]): + def __call__(self, window: Window, result: _DialogResultT, **kwargs: Any) -> Any: """A handler to invoke when a dialog is closed. :param window: The window that opened the dialog. + :param kwargs: Ensures compatibility with arguments added in future versions. :param result: The result returned by the dialog. """ -class DialogResultHandlerAsync(Protocol[_DialogResultT]): - async def __call__(self, window: Window, result: _DialogResultT, /) -> Any: - """Async definition of :any:`DialogResultHandlerSync`.""" - - -class DialogResultHandlerGenerator(Protocol[_DialogResultT]): - def __call__( - self, window: Window, result: _DialogResultT, / - ) -> HandlerGeneratorReturnT[Any]: - """Generator definition of :any:`DialogResultHandlerSync`.""" - - -DialogResultHandlerT: TypeAlias = Union[ - DialogResultHandlerSync[_DialogResultT], - DialogResultHandlerAsync[_DialogResultT], - DialogResultHandlerGenerator[_DialogResultT], -] - - class Dialog(AsyncResult): RESULT_TYPE = "dialog" - def __init__(self, window: Window, on_result: DialogResultHandlerT[Any]): + def __init__(self, window: Window, on_result: DialogResultHandler[Any]): # TODO:PR: should DialogResultHandlerT include the "exception" arg... super().__init__(on_result=on_result) self.window = window @@ -157,7 +123,7 @@ def __init__( resizable: bool = True, closable: bool = True, minimizable: bool = True, - on_close: OnCloseHandlerT | None = None, + on_close: OnCloseHandler | None = None, content: Widget | None = None, resizeable: None = None, # DEPRECATED closeable: None = None, # DEPRECATED @@ -514,7 +480,7 @@ def on_close(self) -> WrappedHandlerT | None: return self._on_close @on_close.setter - def on_close(self, handler: OnCloseHandlerT | None) -> None: + def on_close(self, handler: OnCloseHandler | None) -> None: def cleanup(window: Window, should_close: bool) -> None: if should_close or handler is None: window.close() @@ -529,7 +495,7 @@ def info_dialog( self, title: str, message: str, - on_result: DialogResultHandlerT[None] | None = None, + on_result: DialogResultHandler[None] | None = None, ) -> Dialog: """Ask the user to acknowledge some information. @@ -556,7 +522,7 @@ def question_dialog( self, title: str, message: str, - on_result: DialogResultHandlerT[bool] | None = None, + on_result: DialogResultHandler[bool] | None = None, ) -> Dialog: """Ask the user a yes/no question. @@ -583,7 +549,7 @@ def confirm_dialog( self, title: str, message: str, - on_result: DialogResultHandlerT[bool] | None = None, + on_result: DialogResultHandler[bool] | None = None, ) -> Dialog: """Ask the user to confirm if they wish to proceed with an action. @@ -611,7 +577,7 @@ def error_dialog( self, title: str, message: str, - on_result: DialogResultHandlerT[None] | None = None, + on_result: DialogResultHandler[None] | None = None, ) -> Dialog: """Ask the user to acknowledge an error state. @@ -641,7 +607,7 @@ def stack_trace_dialog( message: str, content: str, retry: Literal[False] = False, - on_result: DialogResultHandlerT[None] | None = None, + on_result: DialogResultHandler[None] | None = None, ) -> Dialog: ... @overload @@ -651,7 +617,7 @@ def stack_trace_dialog( message: str, content: str, retry: Literal[True] = True, - on_result: DialogResultHandlerT[bool] | None = None, + on_result: DialogResultHandler[bool] | None = None, ) -> Dialog: ... @overload @@ -661,9 +627,7 @@ def stack_trace_dialog( message: str, content: str, retry: bool = False, - on_result: ( - DialogResultHandlerT[bool] | DialogResultHandlerT[None] | None - ) = None, + on_result: DialogResultHandler[bool] | DialogResultHandler[None] | None = None, ) -> Dialog: ... def stack_trace_dialog( @@ -672,9 +636,7 @@ def stack_trace_dialog( message: str, content: str, retry: bool = False, - on_result: ( - DialogResultHandlerT[bool] | DialogResultHandlerT[None] | None - ) = None, + on_result: DialogResultHandler[bool] | DialogResultHandler[None] | None = None, ) -> Dialog: """Open a dialog to display a large block of text, such as a stack trace. @@ -710,7 +672,7 @@ def save_file_dialog( title: str, suggested_filename: Path | str, file_types: list[str] | None = None, - on_result: DialogResultHandlerT[Path | None] | None = None, + on_result: DialogResultHandler[Path | None] | None = None, ) -> Dialog: """Prompt the user for a location to save a file. @@ -757,9 +719,7 @@ def open_file_dialog( initial_directory: Path | str | None = None, file_types: list[str] | None = None, multiple_select: Literal[False] = False, - on_result: ( - DialogResultHandlerT[Path] | DialogResultHandlerT[None] | None - ) = None, + on_result: DialogResultHandler[Path] | DialogResultHandler[None] | None = None, multiselect: None = None, # DEPRECATED ) -> Dialog: ... @@ -771,7 +731,7 @@ def open_file_dialog( file_types: list[str] | None = None, multiple_select: Literal[True] = True, on_result: ( - DialogResultHandlerT[list[Path]] | DialogResultHandlerT[None] | None + DialogResultHandler[list[Path]] | DialogResultHandler[None] | None ) = None, multiselect: None = None, # DEPRECATED ) -> Dialog: ... @@ -784,9 +744,9 @@ def open_file_dialog( file_types: list[str] | None = None, multiple_select: bool = False, on_result: ( - DialogResultHandlerT[list[Path]] - | DialogResultHandlerT[Path] - | DialogResultHandlerT[None] + DialogResultHandler[list[Path]] + | DialogResultHandler[Path] + | DialogResultHandler[None] | None ) = None, multiselect: None = None, # DEPRECATED @@ -799,9 +759,9 @@ def open_file_dialog( file_types: list[str] | None = None, multiple_select: bool = False, on_result: ( - DialogResultHandlerT[list[Path]] - | DialogResultHandlerT[Path] - | DialogResultHandlerT[None] + DialogResultHandler[list[Path]] + | DialogResultHandler[Path] + | DialogResultHandler[None] | None ) = None, multiselect: None = None, # DEPRECATED @@ -860,9 +820,7 @@ def select_folder_dialog( title: str, initial_directory: Path | str | None = None, multiple_select: Literal[False] = False, - on_result: ( - DialogResultHandlerT[Path] | DialogResultHandlerT[None] | None - ) = None, + on_result: DialogResultHandler[Path] | DialogResultHandler[None] | None = None, multiselect: None = None, # DEPRECATED ) -> Dialog: ... @@ -873,7 +831,7 @@ def select_folder_dialog( initial_directory: Path | str | None = None, multiple_select: Literal[True] = True, on_result: ( - DialogResultHandlerT[list[Path]] | DialogResultHandlerT[None] | None + DialogResultHandler[list[Path]] | DialogResultHandler[None] | None ) = None, multiselect: None = None, # DEPRECATED ) -> Dialog: ... @@ -885,9 +843,9 @@ def select_folder_dialog( initial_directory: Path | str | None = None, multiple_select: bool = False, on_result: ( - DialogResultHandlerT[list[Path]] - | DialogResultHandlerT[Path] - | DialogResultHandlerT[None] + DialogResultHandler[list[Path]] + | DialogResultHandler[Path] + | DialogResultHandler[None] | None ) = None, multiselect: None = None, # DEPRECATED @@ -899,9 +857,9 @@ def select_folder_dialog( initial_directory: Path | str | None = None, multiple_select: bool = False, on_result: ( - DialogResultHandlerT[list[Path]] - | DialogResultHandlerT[Path] - | DialogResultHandlerT[None] + DialogResultHandler[list[Path]] + | DialogResultHandler[Path] + | DialogResultHandler[None] | None ) = None, multiselect: None = None, # DEPRECATED diff --git a/docs/reference/api/app.rst b/docs/reference/api/app.rst index 28330c3069..44505248dc 100644 --- a/docs/reference/api/app.rst +++ b/docs/reference/api/app.rst @@ -86,9 +86,5 @@ Reference .. autoclass:: toga.App .. autoprotocol:: toga.app.AppStartupMethod -.. autoprotocol:: toga.app.BackgroundTaskSync -.. autoprotocol:: toga.app.BackgroundTaskAsync -.. autoprotocol:: toga.app.BackgroundTaskGenerator -.. autoprotocol:: toga.app.OnExitHandlerSync -.. autoprotocol:: toga.app.OnExitHandlerAsync -.. autoprotocol:: toga.app.OnExitHandlerGenerator +.. autoprotocol:: toga.app.BackgroundTask +.. autoprotocol:: toga.app.OnExitHandler diff --git a/docs/reference/api/containers/optioncontainer.rst b/docs/reference/api/containers/optioncontainer.rst index 4bd1bbdb1b..728a53d51e 100644 --- a/docs/reference/api/containers/optioncontainer.rst +++ b/docs/reference/api/containers/optioncontainer.rst @@ -195,6 +195,4 @@ Reference .. autoclass:: toga.widgets.optioncontainer.OptionItem -.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandlerSync -.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandlerAsync -.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandlerGenerator +.. autoprotocol:: toga.widgets.optioncontainer.OnSelectHandler diff --git a/docs/reference/api/containers/scrollcontainer.rst b/docs/reference/api/containers/scrollcontainer.rst index 368c3bbbb9..53020130d2 100644 --- a/docs/reference/api/containers/scrollcontainer.rst +++ b/docs/reference/api/containers/scrollcontainer.rst @@ -61,6 +61,4 @@ Reference .. autoclass:: toga.ScrollContainer :exclude-members: window, app -.. autoprotocol:: toga.widgets.scrollcontainer.OnScrollHandlerSync -.. autoprotocol:: toga.widgets.scrollcontainer.OnScrollHandlerAsync -.. autoprotocol:: toga.widgets.scrollcontainer.OnScrollHandlerGenerator +.. autoprotocol:: toga.widgets.scrollcontainer.OnScrollHandler diff --git a/docs/reference/api/resources/command.rst b/docs/reference/api/resources/command.rst index cb27a0851d..9f2067bfcb 100644 --- a/docs/reference/api/resources/command.rst +++ b/docs/reference/api/resources/command.rst @@ -83,6 +83,4 @@ Reference .. autoclass:: toga.Group :exclude-members: key -.. autoprotocol:: toga.command.ActionHandlerSync -.. autoprotocol:: toga.command.ActionHandlerAsync -.. autoprotocol:: toga.command.ActionHandlerGenerator +.. autoprotocol:: toga.command.ActionHandler diff --git a/docs/reference/api/widgets/button.rst b/docs/reference/api/widgets/button.rst index 890ee25648..ba1aab9694 100644 --- a/docs/reference/api/widgets/button.rst +++ b/docs/reference/api/widgets/button.rst @@ -85,6 +85,4 @@ Reference .. autoclass:: toga.Button -.. autoprotocol:: toga.widgets.button.OnPressHandlerSync -.. autoprotocol:: toga.widgets.button.OnPressHandlerAsync -.. autoprotocol:: toga.widgets.button.OnPressHandlerGenerator +.. autoprotocol:: toga.widgets.button.OnPressHandler diff --git a/docs/reference/api/widgets/dateinput.rst b/docs/reference/api/widgets/dateinput.rst index 2e40c276f0..c0da1d954d 100644 --- a/docs/reference/api/widgets/dateinput.rst +++ b/docs/reference/api/widgets/dateinput.rst @@ -61,6 +61,4 @@ Reference .. autoclass:: toga.DateInput -.. autoprotocol:: toga.widgets.dateinput.OnChangeHandlerSync -.. autoprotocol:: toga.widgets.dateinput.OnChangeHandlerAsync -.. autoprotocol:: toga.widgets.dateinput.OnChangeHandlerGenerator +.. autoprotocol:: toga.widgets.dateinput.OnChangeHandler diff --git a/docs/reference/api/widgets/detailedlist.rst b/docs/reference/api/widgets/detailedlist.rst index f32f6a7587..306d858e58 100644 --- a/docs/reference/api/widgets/detailedlist.rst +++ b/docs/reference/api/widgets/detailedlist.rst @@ -147,15 +147,7 @@ Reference .. autoclass:: toga.DetailedList -.. autoprotocol:: toga.widgets.detailedlist.OnPrimaryActionHandlerSync -.. autoprotocol:: toga.widgets.detailedlist.OnPrimaryActionHandlerAsync -.. autoprotocol:: toga.widgets.detailedlist.OnPrimaryActionHandlerGenerator -.. autoprotocol:: toga.widgets.detailedlist.OnSecondaryActionHandlerSync -.. autoprotocol:: toga.widgets.detailedlist.OnSecondaryActionHandlerAsync -.. autoprotocol:: toga.widgets.detailedlist.OnSecondaryActionHandlerGenerator -.. autoprotocol:: toga.widgets.detailedlist.OnRefreshHandlerSync -.. autoprotocol:: toga.widgets.detailedlist.OnRefreshHandlerAsync -.. autoprotocol:: toga.widgets.detailedlist.OnRefreshHandlerGenerator -.. autoprotocol:: toga.widgets.detailedlist.OnSelectHandlerSync -.. autoprotocol:: toga.widgets.detailedlist.OnSelectHandlerAsync -.. autoprotocol:: toga.widgets.detailedlist.OnSelectHandlerGenerator +.. autoprotocol:: toga.widgets.detailedlist.OnPrimaryActionHandler +.. autoprotocol:: toga.widgets.detailedlist.OnSecondaryActionHandler +.. autoprotocol:: toga.widgets.detailedlist.OnRefreshHandler +.. autoprotocol:: toga.widgets.detailedlist.OnSelectHandler diff --git a/docs/reference/api/widgets/mapview.rst b/docs/reference/api/widgets/mapview.rst index 2098acd019..ad383e1c57 100644 --- a/docs/reference/api/widgets/mapview.rst +++ b/docs/reference/api/widgets/mapview.rst @@ -145,7 +145,4 @@ Reference .. autoclass:: toga.widgets.mapview.MapPinSet -.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerSync -.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerAsync -.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerGenerator -.. autoprotocol:: toga.widgets.mapview.OnSelectHandlerT +.. autoprotocol:: toga.widgets.mapview.OnSelectHandler diff --git a/docs/reference/api/widgets/multilinetextinput.rst b/docs/reference/api/widgets/multilinetextinput.rst index bfc17d6bf0..8c69cd4cd2 100644 --- a/docs/reference/api/widgets/multilinetextinput.rst +++ b/docs/reference/api/widgets/multilinetextinput.rst @@ -72,6 +72,4 @@ Reference .. autoclass:: toga.MultilineTextInput -.. autoprotocol:: toga.widgets.multilinetextinput.OnChangeHandlerSync -.. autoprotocol:: toga.widgets.multilinetextinput.OnChangeHandlerAsync -.. autoprotocol:: toga.widgets.multilinetextinput.OnChangeHandlerGenerator +.. autoprotocol:: toga.widgets.multilinetextinput.OnChangeHandler diff --git a/docs/reference/api/widgets/numberinput.rst b/docs/reference/api/widgets/numberinput.rst index 5129146bd3..950aad45b0 100644 --- a/docs/reference/api/widgets/numberinput.rst +++ b/docs/reference/api/widgets/numberinput.rst @@ -62,6 +62,4 @@ Reference .. autoclass:: toga.NumberInput -.. autoprotocol:: toga.widgets.numberinput.OnChangeHandlerSync -.. autoprotocol:: toga.widgets.numberinput.OnChangeHandlerAsync -.. autoprotocol:: toga.widgets.numberinput.OnChangeHandlerGenerator +.. autoprotocol:: toga.widgets.numberinput.OnChangeHandler diff --git a/docs/reference/api/widgets/slider.rst b/docs/reference/api/widgets/slider.rst index a52fad5133..c021103806 100644 --- a/docs/reference/api/widgets/slider.rst +++ b/docs/reference/api/widgets/slider.rst @@ -70,12 +70,6 @@ Reference .. autoclass:: toga.Slider -.. autoprotocol:: toga.widgets.slider.OnChangeHandlerSync -.. autoprotocol:: toga.widgets.slider.OnChangeHandlerAsync -.. autoprotocol:: toga.widgets.slider.OnChangeHandlerGenerator -.. autoprotocol:: toga.widgets.slider.OnPressHandlerSync -.. autoprotocol:: toga.widgets.slider.OnPressHandlerAsync -.. autoprotocol:: toga.widgets.slider.OnPressHandlerGenerator -.. autoprotocol:: toga.widgets.slider.OnReleaseHandlerSync -.. autoprotocol:: toga.widgets.slider.OnReleaseHandlerAsync -.. autoprotocol:: toga.widgets.slider.OnReleaseHandlerGenerator +.. autoprotocol:: toga.widgets.slider.OnChangeHandler +.. autoprotocol:: toga.widgets.slider.OnPressHandler +.. autoprotocol:: toga.widgets.slider.OnReleaseHandler diff --git a/docs/reference/api/widgets/switch.rst b/docs/reference/api/widgets/switch.rst index 77662b6aa1..38d8d87e74 100644 --- a/docs/reference/api/widgets/switch.rst +++ b/docs/reference/api/widgets/switch.rst @@ -85,6 +85,4 @@ Reference .. autoclass:: toga.Switch -.. autoprotocol:: toga.widgets.switch.OnChangeHandlerSync -.. autoprotocol:: toga.widgets.switch.OnChangeHandlerAsync -.. autoprotocol:: toga.widgets.switch.OnChangeHandlerGenerator +.. autoprotocol:: toga.widgets.switch.OnChangeHandler diff --git a/docs/reference/api/widgets/table.rst b/docs/reference/api/widgets/table.rst index 0df9acbe2d..a910c688d9 100644 --- a/docs/reference/api/widgets/table.rst +++ b/docs/reference/api/widgets/table.rst @@ -136,9 +136,5 @@ Reference .. autoclass:: toga.Table -.. autoprotocol:: toga.widgets.table.OnSelectHandlerSync -.. autoprotocol:: toga.widgets.table.OnSelectHandlerAsync -.. autoprotocol:: toga.widgets.table.OnSelectHandlerGenerator -.. autoprotocol:: toga.widgets.table.OnActivateHandlerSync -.. autoprotocol:: toga.widgets.table.OnActivateHandlerAsync -.. autoprotocol:: toga.widgets.table.OnActivateHandlerGenerator +.. autoprotocol:: toga.widgets.table.OnSelectHandler +.. autoprotocol:: toga.widgets.table.OnActivateHandler diff --git a/docs/reference/api/widgets/textinput.rst b/docs/reference/api/widgets/textinput.rst index 436e86a9e2..49512eb092 100644 --- a/docs/reference/api/widgets/textinput.rst +++ b/docs/reference/api/widgets/textinput.rst @@ -95,15 +95,7 @@ Reference .. autoclass:: toga.TextInput -.. autoprotocol:: toga.widgets.textinput.OnChangeHandlerSync -.. autoprotocol:: toga.widgets.textinput.OnChangeHandlerAsync -.. autoprotocol:: toga.widgets.textinput.OnChangeHandlerGenerator -.. autoprotocol:: toga.widgets.textinput.OnConfirmHandlerSync -.. autoprotocol:: toga.widgets.textinput.OnConfirmHandlerAsync -.. autoprotocol:: toga.widgets.textinput.OnConfirmHandlerGenerator -.. autoprotocol:: toga.widgets.textinput.OnGainFocusHandlerSync -.. autoprotocol:: toga.widgets.textinput.OnGainFocusHandlerAsync -.. autoprotocol:: toga.widgets.textinput.OnGainFocusHandlerGenerator -.. autoprotocol:: toga.widgets.textinput.OnLoseFocusHandlerSync -.. autoprotocol:: toga.widgets.textinput.OnLoseFocusHandlerAsync -.. autoprotocol:: toga.widgets.textinput.OnLoseFocusHandlerGenerator +.. autoprotocol:: toga.widgets.textinput.OnChangeHandler +.. autoprotocol:: toga.widgets.textinput.OnConfirmHandler +.. autoprotocol:: toga.widgets.textinput.OnGainFocusHandler +.. autoprotocol:: toga.widgets.textinput.OnLoseFocusHandler diff --git a/docs/reference/api/widgets/timeinput.rst b/docs/reference/api/widgets/timeinput.rst index 205eaabf55..1218cc4f45 100644 --- a/docs/reference/api/widgets/timeinput.rst +++ b/docs/reference/api/widgets/timeinput.rst @@ -65,6 +65,4 @@ Reference .. autoclass:: toga.TimeInput -.. autoprotocol:: toga.widgets.timeinput.OnChangeHandlerSync -.. autoprotocol:: toga.widgets.timeinput.OnChangeHandlerAsync -.. autoprotocol:: toga.widgets.timeinput.OnChangeHandlerGenerator +.. autoprotocol:: toga.widgets.timeinput.OnChangeHandler diff --git a/docs/reference/api/widgets/tree.rst b/docs/reference/api/widgets/tree.rst index 4174c5909a..cba03f43e1 100644 --- a/docs/reference/api/widgets/tree.rst +++ b/docs/reference/api/widgets/tree.rst @@ -153,9 +153,5 @@ Reference .. autoclass:: toga.Tree -.. autoprotocol:: toga.widgets.tree.OnSelectHandlerSync -.. autoprotocol:: toga.widgets.tree.OnSelectHandlerAsync -.. autoprotocol:: toga.widgets.tree.OnSelectHandlerGenerator -.. autoprotocol:: toga.widgets.tree.OnActivateHandlerSync -.. autoprotocol:: toga.widgets.tree.OnActivateHandlerAsync -.. autoprotocol:: toga.widgets.tree.OnActivateHandlerGenerator +.. autoprotocol:: toga.widgets.tree.OnSelectHandler +.. autoprotocol:: toga.widgets.tree.OnActivateHandler diff --git a/docs/reference/api/widgets/webview.rst b/docs/reference/api/widgets/webview.rst index a0a495866a..ccc035fa9d 100644 --- a/docs/reference/api/widgets/webview.rst +++ b/docs/reference/api/widgets/webview.rst @@ -110,6 +110,4 @@ Reference .. autoclass:: toga.WebView -.. autoprotocol:: toga.widgets.webview.OnWebViewLoadHandlerSync -.. autoprotocol:: toga.widgets.webview.OnWebViewLoadHandlerAsync -.. autoprotocol:: toga.widgets.webview.OnWebViewLoadHandlerGenerator +.. autoprotocol:: toga.widgets.webview.OnWebViewLoadHandler diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index c73803c063..6cf24d63b1 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -97,9 +97,5 @@ Reference .. autoclass:: toga.Window .. autoprotocol:: toga.window.Dialog -.. autoprotocol:: toga.window.OnCloseHandlerSync -.. autoprotocol:: toga.window.OnCloseHandlerAsync -.. autoprotocol:: toga.window.OnCloseHandlerGenerator -.. autoprotocol:: toga.window.DialogResultHandlerSync -.. autoprotocol:: toga.window.DialogResultHandlerAsync -.. autoprotocol:: toga.window.DialogResultHandlerGenerator +.. autoprotocol:: toga.window.OnCloseHandler +.. autoprotocol:: toga.window.DialogResultHandler From c198f31ba0073f3e681eec62057af70c5e15dca7 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 4 Jun 2024 13:17:16 -0400 Subject: [PATCH 05/13] Use PEP 585 abstract types --- core/src/toga/app.py | 8 ++++---- core/src/toga/handlers.py | 14 +++----------- core/src/toga/sources/tree_source.py | 4 ++-- core/src/toga/widgets/detailedlist.py | 2 +- core/src/toga/widgets/multilinetextinput.py | 2 +- core/src/toga/widgets/numberinput.py | 2 +- core/src/toga/widgets/selection.py | 2 +- core/src/toga/widgets/switch.py | 2 +- 8 files changed, 14 insertions(+), 22 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 657e2c89fd..7864d38412 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -6,10 +6,10 @@ import sys import warnings import webbrowser -from collections.abc import Iterator +from collections.abc import Iterator, MutableSet, Set from email.message import Message from pathlib import Path -from typing import TYPE_CHECKING, AbstractSet, Any, MutableSet, Protocol +from typing import TYPE_CHECKING, Any, Protocol from warnings import warn from weakref import WeakValueDictionary @@ -100,7 +100,7 @@ def discard(self, window: Window) -> None: # 2023-10: Backwards compatibility ###################################################################### - def __iadd__(self, window: AbstractSet[Any]) -> WindowSet: + def __iadd__(self, window: Set[Any]) -> WindowSet: # The standard set type does not have a += operator. warn( "Windows are automatically associated with the app; += is not required", @@ -109,7 +109,7 @@ def __iadd__(self, window: AbstractSet[Any]) -> WindowSet: ) return self - def __isub__(self, other: AbstractSet[Any]) -> WindowSet: + def __isub__(self, other: Set[Any]) -> WindowSet: # The standard set type does have a -= operator, but it takes sets rather than # individual items. warn( diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index ea707247b1..c69e3f315e 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -6,16 +6,8 @@ import traceback import warnings from abc import ABC -from typing import ( - Any, - Awaitable, - Callable, - Generator, - NoReturn, - Protocol, - TypeVar, - Union, -) +from collections.abc import Awaitable, Callable, Generator +from typing import Any, NoReturn, Protocol, TypeVar, Union from toga.types import TypeAlias @@ -184,7 +176,7 @@ def __init__(self, on_result: OnResultT | None = None) -> None: # End backwards compatibility. ###################################################################### - def set_result(self, result: Any) -> None: + def set_result(self, result: object) -> None: if not self.future.cancelled(): self.future.set_result(result) if self.on_result: diff --git a/core/src/toga/sources/tree_source.py b/core/src/toga/sources/tree_source.py index 2329a38ef4..775bb9cab4 100644 --- a/core/src/toga/sources/tree_source.py +++ b/core/src/toga/sources/tree_source.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Iterator -from typing import Generic, Iterable, Mapping, Tuple, TypeVar, Union +from collections.abc import Iterable, Iterator, Mapping +from typing import Generic, Tuple, TypeVar, Union from toga.types import TypeAlias diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 752f17cc0c..74f9ed3cc7 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -10,7 +10,7 @@ TypeVar, ) -import toga.widgets.detailedlist +import toga from toga.handlers import WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Row, Source diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index c54d4cc8b6..f661bfc7fc 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -2,7 +2,7 @@ from typing import Any, Protocol -import toga.widgets.multilinetextinput +import toga from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 0cf3f50305..92a3fe8055 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -5,7 +5,7 @@ from decimal import ROUND_HALF_UP, Decimal, InvalidOperation from typing import Any, Protocol, Union -import toga.widgets.numberinput +import toga from toga.handlers import WrappedHandlerT, wrapped_handler from toga.types import TypeAlias diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index a653b60ea0..ee164203d0 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -4,7 +4,7 @@ from collections.abc import Collection from typing import Any, Generic, Protocol, TypeVar -import toga.widgets.selection +import toga from toga.handlers import WrappedHandlerT, wrapped_handler from toga.sources import ListSource, Source diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index b6b6fc79a2..f4ad3511ab 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -2,7 +2,7 @@ from typing import Any, Protocol -import toga.widgets.switch +import toga from toga.handlers import WrappedHandlerT, wrapped_handler from .base import StyleT, Widget From 9d316b45db16b244425f2eeb4feb80437ca0233a Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 4 Jun 2024 14:57:48 -0400 Subject: [PATCH 06/13] Use loosest abstract type and ensure it's supported --- core/src/toga/sources/accessors.py | 11 ++++++----- core/src/toga/sources/list_source.py | 14 +++++++------- core/src/toga/sources/tree_source.py | 2 +- core/src/toga/widgets/box.py | 6 +++--- core/src/toga/widgets/detailedlist.py | 4 ++-- core/src/toga/widgets/mapview.py | 9 +++++---- core/src/toga/widgets/optioncontainer.py | 6 +++--- core/src/toga/widgets/selection.py | 6 +++--- core/src/toga/widgets/table.py | 9 +++++---- core/src/toga/widgets/tree.py | 4 ++-- 10 files changed, 37 insertions(+), 34 deletions(-) diff --git a/core/src/toga/sources/accessors.py b/core/src/toga/sources/accessors.py index 9824f31238..d9fc673711 100644 --- a/core/src/toga/sources/accessors.py +++ b/core/src/toga/sources/accessors.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from collections.abc import Collection, Mapping +from collections.abc import Iterable, Mapping NON_ACCESSOR_CHARS = re.compile(r"[^\w ]") WHITESPACE = re.compile(r"\s+") @@ -46,8 +46,8 @@ def to_accessor(heading: str) -> str: def build_accessors( - headings: Collection[str], - accessors: Collection[str | None] | Mapping[str, str] | None, + headings: Iterable[str], + accessors: Iterable[str | None] | Mapping[str, str] | None, ) -> list[str]: """Convert a list of headings (with accessor overrides) to a finalised list of accessors. @@ -64,12 +64,13 @@ def build_accessors( :returns: The final list of accessors. """ if accessors: - if isinstance(accessors, dict): + if isinstance(accessors, Mapping): result = [ accessors[h] if h in accessors else to_accessor(h) for h in headings ] else: - if len(headings) != len(accessors): + # TODO: use zip(..., strict=True) instead once Python 3.9 support is dropped + if len(headings := list(headings)) != len(accessors := list(accessors)): raise ValueError("Number of accessors must match number of headings") result = [ diff --git a/core/src/toga/sources/list_source.py b/core/src/toga/sources/list_source.py index 4212a7d359..873b992a06 100644 --- a/core/src/toga/sources/list_source.py +++ b/core/src/toga/sources/list_source.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Collection, Iterable +from collections.abc import Iterable, Mapping, Sequence from typing import Generic, TypeVar from .base import Source @@ -9,9 +9,9 @@ def _find_item( - candidates: list[T], + candidates: Sequence[T], data: object, - accessors: list[str], + accessors: Sequence[str], start: T | None, error: str, ) -> T: @@ -24,7 +24,7 @@ def _find_item( for item in candidates[start_index:]: try: - if isinstance(data, dict): + if isinstance(data, Mapping): found = all( getattr(item, attr) == value for attr, value in data.items() ) @@ -95,7 +95,7 @@ def __delattr__(self, attr: str) -> None: # TODO:PR: consider adding supported Protocols...maybe List? class ListSource(Source, Generic[T]): - def __init__(self, accessors: Iterable[str], data: Collection[T] | None = None): + def __init__(self, accessors: Iterable[str], data: Iterable[T] | None = None): """A data source to store an ordered list of multiple data values. :param accessors: A list of attribute names for accessing the value @@ -113,7 +113,7 @@ def __init__(self, accessors: Iterable[str], data: Collection[T] | None = None): raise ValueError("ListSource must be provided a list of accessors") # Convert the data into row objects - if data: + if data is not None: self._data = [self._create_row(value) for value in data] else: self._data = [] @@ -142,7 +142,7 @@ def __delitem__(self, index: int) -> None: # This behavior is documented in list_source.rst. def _create_row(self, data: T) -> Row[T]: - if isinstance(data, dict): + if isinstance(data, Mapping): row = Row(**data) elif hasattr(data, "__iter__") and not isinstance(data, str): row = Row(**dict(zip(self._accessors, data))) diff --git a/core/src/toga/sources/tree_source.py b/core/src/toga/sources/tree_source.py index 775bb9cab4..2703fd4eb9 100644 --- a/core/src/toga/sources/tree_source.py +++ b/core/src/toga/sources/tree_source.py @@ -218,7 +218,7 @@ def __init__( if len(self._accessors) == 0: raise ValueError("TreeSource must be provided a list of accessors") - if data: + if data is not None: self._roots: list[Node[T]] = self._create_nodes(parent=None, value=data) else: self._roots = [] diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index ab940e3a31..5f84160fb1 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Collection +from collections.abc import Iterable from .base import StyleT, Widget @@ -13,7 +13,7 @@ def __init__( self, id: str | None = None, style: StyleT | None = None, - children: Collection[Widget] | None = None, + children: Iterable[Widget] | None = None, ): """Create a new Box container widget. @@ -29,7 +29,7 @@ def __init__( # Children need to be added *after* the impl has been created. self._children: list[Widget] = [] - if children: + if children is not None: self.add(*children) @property diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 74f9ed3cc7..966ae01643 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from collections.abc import Collection, Iterable +from collections.abc import Iterable from typing import ( Any, Generic, @@ -157,7 +157,7 @@ def data(self) -> SourceT | ListSource[T]: return self._data @data.setter - def data(self, data: SourceT | Collection[T] | None) -> None: + def data(self, data: SourceT | Iterable[T] | None) -> None: if data is None: self._data = ListSource(data=[], accessors=self.accessors) elif isinstance(data, Source): diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 3c753ca487..43b7a82b17 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Collection, Iterator +from collections.abc import Iterable, Iterator from typing import Any, Protocol import toga @@ -75,12 +75,13 @@ def subtitle(self, subtitle: str | None) -> None: self.interface._impl.update_pin(self) +# TODO:PR: __contains__() is required to subclass Set; should it not be added? class MapPinSet: - def __init__(self, interface: MapView, pins: Collection[MapPin] | None): + def __init__(self, interface: MapView, pins: Iterable[MapPin] | None): self.interface = interface self._pins: set[MapPin] = set() - if pins: + if pins is not None: for item in pins: self.add(item) @@ -137,7 +138,7 @@ def __init__( style: StyleT | None = None, location: toga.LatLng | tuple[float, float] | None = None, zoom: int = 11, - pins: Collection[MapPin] | None = None, + pins: Iterable[MapPin] | None = None, on_select: toga.widgets.mapview.OnSelectHandler | None = None, ): """Create a new MapView widget. diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index c40acfb60c..82976d4bfb 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Collection +from collections.abc import Iterable from typing import ( TYPE_CHECKING, Any, @@ -380,7 +380,7 @@ def __init__( self, id: str | None = None, style: StyleT | None = None, - content: Collection[tuple[str, Widget]] | None = None, + content: Iterable[tuple[str, Widget]] | None = None, on_select: toga.widgets.optioncontainer.OnSelectHandler | None = None, ): """Create a new OptionContainer. @@ -398,7 +398,7 @@ def __init__( self._impl = self.factory.OptionContainer(interface=self) - if content: + if content is not None: for item in content: if isinstance(item, OptionItem): self.content.append(item) diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index ee164203d0..ff55ec174f 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from collections.abc import Collection +from collections.abc import Iterable from typing import Any, Generic, Protocol, TypeVar import toga @@ -27,7 +27,7 @@ def __init__( self, id: str | None = None, style: StyleT | None = None, - items: SourceT | Collection[T] | None = None, + items: SourceT | Iterable[T] | None = None, accessor: str | None = None, value: T | None = None, on_change: toga.widgets.selection.OnChangeHandler | None = None, @@ -98,7 +98,7 @@ def items(self) -> SourceT | ListSource[T]: return self._items @items.setter - def items(self, items: SourceT | Collection[T] | None) -> None: + def items(self, items: SourceT | Iterable[T] | None) -> None: if self._accessor is None: accessors = ["value"] else: diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 89607ac7e9..e59d689542 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from collections.abc import Collection, Iterable, MutableSequence +from collections.abc import Iterable from typing import Any, Generic, Literal, Protocol, TypeVar import toga @@ -38,7 +38,7 @@ def __init__( id: str | None = None, style: StyleT | None = None, data: SourceT | Iterable[T] | None = None, - accessors: MutableSequence[str] | None = None, + accessors: Iterable[str] | None = None, multiple_select: bool = False, on_select: toga.widgets.table.OnSelectHandler | None = None, on_activate: toga.widgets.table.OnActivateHandler | None = None, @@ -95,6 +95,7 @@ def __init__( ###################################################################### self._headings: list[str] | None + self._accessors: list[str] self._data: SourceT | ListSource[T] if headings is not None: @@ -102,7 +103,7 @@ def __init__( self._accessors = build_accessors(self._headings, accessors) elif accessors is not None: self._headings = None - self._accessors = accessors + self._accessors = list(accessors) else: raise ValueError( "Cannot create a table without either headings or accessors" @@ -155,7 +156,7 @@ def data(self) -> ListSource[T]: return self._data @data.setter - def data(self, data: SourceT | Collection[T] | None) -> None: + def data(self, data: SourceT | Iterable[T] | None) -> None: if data is None: self._data = ListSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 9b8617ee9f..489add9925 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from collections.abc import Collection, Iterable +from collections.abc import Iterable from typing import Any, Generic, Literal, Protocol, TypeVar import toga @@ -40,7 +40,7 @@ def __init__( id: str | None = None, style: Pack | None = None, data: SourceT | TreeSourceDataT[T] | None = None, - accessors: Collection[str] | None = None, + accessors: Iterable[str] | None = None, multiple_select: bool = False, on_select: toga.widgets.tree.OnSelectHandler | None = None, on_activate: toga.widgets.tree.OnActivateHandler | None = None, From a0005dfd009bac73dc4d4570f2395028368d80e7 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 4 Jun 2024 15:40:16 -0400 Subject: [PATCH 07/13] Revert some PEP 585 abstract types for Python 3.8 --- core/src/toga/app.py | 4 ++-- core/src/toga/handlers.py | 12 ++++++++++-- core/src/toga/sources/tree_source.py | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 7864d38412..d770752cdd 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -6,10 +6,10 @@ import sys import warnings import webbrowser -from collections.abc import Iterator, MutableSet, Set +from collections.abc import Iterator, Set from email.message import Message from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Any, MutableSet, Protocol from warnings import warn from weakref import WeakValueDictionary diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index c69e3f315e..5477b2b64c 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -6,8 +6,16 @@ import traceback import warnings from abc import ABC -from collections.abc import Awaitable, Callable, Generator -from typing import Any, NoReturn, Protocol, TypeVar, Union +from typing import ( + Any, + Awaitable, + Callable, + Generator, + NoReturn, + Protocol, + TypeVar, + Union, +) from toga.types import TypeAlias diff --git a/core/src/toga/sources/tree_source.py b/core/src/toga/sources/tree_source.py index 2703fd4eb9..45b6828783 100644 --- a/core/src/toga/sources/tree_source.py +++ b/core/src/toga/sources/tree_source.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Iterable, Iterator, Mapping -from typing import Generic, Tuple, TypeVar, Union +from collections.abc import Iterator +from typing import Generic, Iterable, Mapping, Tuple, TypeVar, Union from toga.types import TypeAlias From 0d736a41cfd8a083a91f3ecc4ff9888b6deead5d Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 4 Jun 2024 17:33:30 -0400 Subject: [PATCH 08/13] Handler getters return type is their sync Protocol --- core/src/toga/app.py | 4 ++-- core/src/toga/command.py | 4 ++-- core/src/toga/hardware/location.py | 9 ++------- core/src/toga/widgets/button.py | 4 ++-- core/src/toga/widgets/canvas.py | 18 +++++++++--------- core/src/toga/widgets/dateinput.py | 4 ++-- core/src/toga/widgets/detailedlist.py | 12 ++++++------ core/src/toga/widgets/mapview.py | 4 ++-- core/src/toga/widgets/multilinetextinput.py | 4 ++-- core/src/toga/widgets/numberinput.py | 4 ++-- core/src/toga/widgets/optioncontainer.py | 4 ++-- core/src/toga/widgets/scrollcontainer.py | 4 ++-- core/src/toga/widgets/selection.py | 7 +++---- core/src/toga/widgets/slider.py | 10 ++++------ core/src/toga/widgets/switch.py | 4 ++-- core/src/toga/widgets/table.py | 8 ++++---- core/src/toga/widgets/textinput.py | 10 +++++----- core/src/toga/widgets/timeinput.py | 4 ++-- core/src/toga/widgets/tree.py | 8 ++++---- core/src/toga/widgets/webview.py | 8 ++------ core/src/toga/window.py | 8 ++------ 21 files changed, 63 insertions(+), 79 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index d770752cdd..b35ac27fb7 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -15,7 +15,7 @@ from toga.command import CommandSet from toga.documents import Document -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.hardware.camera import Camera from toga.hardware.location import Location from toga.icons import Icon @@ -807,7 +807,7 @@ def set_full_screen(self, *windows: Window) -> None: ###################################################################### @property - def on_exit(self) -> WrappedHandlerT: + def on_exit(self) -> OnExitHandler: """The handler to invoke if the user attempts to exit the app.""" return self._on_exit diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 7756fe3f04..257e596900 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -3,7 +3,7 @@ from collections.abc import Iterator from typing import TYPE_CHECKING, Protocol -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.icons import Icon from toga.keys import Key from toga.platform import get_platform_factory @@ -246,7 +246,7 @@ def icon(self, icon_or_name: IconContent | None) -> None: self._icon = Icon(icon_or_name) @property - def action(self) -> WrappedHandlerT | None: + def action(self) -> ActionHandler | None: """The Action attached to the command.""" return self._action diff --git a/core/src/toga/hardware/location.py b/core/src/toga/hardware/location.py index 15d5c8a3d0..6cfb804dff 100644 --- a/core/src/toga/hardware/location.py +++ b/core/src/toga/hardware/location.py @@ -3,12 +3,7 @@ from typing import TYPE_CHECKING, Any, Protocol import toga -from toga.handlers import ( - AsyncResult, - PermissionResult, - WrappedHandlerT, - wrapped_handler, -) +from toga.handlers import AsyncResult, PermissionResult, wrapped_handler from toga.platform import get_platform_factory if TYPE_CHECKING: @@ -136,7 +131,7 @@ def request_background_permission(self) -> PermissionResult: return result @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnLocationChangeHandler: """The handler to invoke when an update to the user's location is available.""" return self._on_change diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index 14b8bb5433..f18b4db11f 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, Protocol import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -133,7 +133,7 @@ def icon(self, value: IconContent | None) -> None: self.refresh() @property - def on_press(self) -> WrappedHandlerT: + def on_press(self) -> OnPressHandler: """The handler to invoke when the button is pressed.""" return self._on_press diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 9c7823c165..83e55b3692 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -24,7 +24,7 @@ SYSTEM_DEFAULT_FONT_SIZE, Font, ) -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -1354,7 +1354,7 @@ def Stroke( return self.context.Stroke(x, y, color, line_width, line_dash) @property - def on_resize(self) -> WrappedHandlerT: + def on_resize(self) -> OnResizeHandler: """The handler to invoke when the canvas is resized.""" return self._on_resize @@ -1363,7 +1363,7 @@ def on_resize(self, handler: OnResizeHandler) -> None: self._on_resize = wrapped_handler(self, handler) @property - def on_press(self) -> WrappedHandlerT: + def on_press(self) -> OnTouchHandler: """The handler invoked when the canvas is pressed. When a mouse is being used, this press will be with the primary (usually the left) mouse button.""" return self._on_press @@ -1373,7 +1373,7 @@ def on_press(self, handler: OnTouchHandler) -> None: self._on_press = wrapped_handler(self, handler) @property - def on_activate(self) -> WrappedHandlerT: + def on_activate(self) -> OnTouchHandler: """The handler invoked when the canvas is pressed in a way indicating the pressed object should be activated. When a mouse is in use, this will usually be a double click with the primary (usually the left) mouse button. @@ -1386,7 +1386,7 @@ def on_activate(self, handler: OnTouchHandler) -> None: self._on_activate = wrapped_handler(self, handler) @property - def on_release(self) -> WrappedHandlerT: + def on_release(self) -> OnTouchHandler: """The handler invoked when a press on the canvas ends.""" return self._on_release @@ -1395,7 +1395,7 @@ def on_release(self, handler: OnTouchHandler) -> None: self._on_release = wrapped_handler(self, handler) @property - def on_drag(self) -> WrappedHandlerT: + def on_drag(self) -> OnTouchHandler: """The handler invoked when the location of a press changes.""" return self._on_drag @@ -1404,7 +1404,7 @@ def on_drag(self, handler: OnTouchHandler) -> None: self._on_drag = wrapped_handler(self, handler) @property - def on_alt_press(self) -> WrappedHandlerT: + def on_alt_press(self) -> OnTouchHandler: """The handler to invoke when the canvas is pressed in an alternate manner. This will usually correspond to a secondary (usually the right) mouse button press. @@ -1418,7 +1418,7 @@ def on_alt_press(self, handler: OnTouchHandler) -> None: self._on_alt_press = wrapped_handler(self, handler) @property - def on_alt_release(self) -> WrappedHandlerT: + def on_alt_release(self) -> OnTouchHandler: """The handler to invoke when an alternate press is released. This event is not supported on Android or iOS. @@ -1430,7 +1430,7 @@ def on_alt_release(self, handler: OnTouchHandler) -> None: self._on_alt_release = wrapped_handler(self, handler) @property - def on_alt_drag(self) -> WrappedHandlerT: + def on_alt_drag(self) -> OnTouchHandler: """The handler to invoke when the location of an alternate press changes. This event is not supported on Android or iOS. diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 1dbb305812..e4e73d2bb4 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -5,7 +5,7 @@ from typing import Any, Protocol import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -154,7 +154,7 @@ def max(self, value: object) -> None: self.value = max @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """The handler to invoke when the date value changes.""" return self._on_change diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 966ae01643..5f19ee64ad 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -11,7 +11,7 @@ ) import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.sources import ListSource, Row, Source from .base import StyleT, Widget @@ -212,7 +212,7 @@ def selection(self) -> Row[T] | None: return None @property - def on_primary_action(self) -> WrappedHandlerT: + def on_primary_action(self) -> OnPrimaryActionHandler: """The handler to invoke when the user performs the primary action on a row of the DetailedList. @@ -230,7 +230,7 @@ def on_primary_action(self, handler: OnPrimaryActionHandler) -> None: self._impl.set_primary_action_enabled(handler is not None) @property - def on_secondary_action(self) -> WrappedHandlerT: + def on_secondary_action(self) -> OnSecondaryActionHandler: """The handler to invoke when the user performs the secondary action on a row of the DetailedList. @@ -248,7 +248,7 @@ def on_secondary_action(self, handler: OnSecondaryActionHandler) -> None: self._impl.set_secondary_action_enabled(handler is not None) @property - def on_refresh(self) -> WrappedHandlerT: + def on_refresh(self) -> OnRefreshHandler: """The callback function to invoke when the user performs a refresh action (usually "pull down") on the DetailedList. @@ -265,7 +265,7 @@ def on_refresh(self, handler: OnRefreshHandler) -> None: self._impl.set_refresh_enabled(handler is not None) @property - def on_select(self) -> WrappedHandlerT: + def on_select(self) -> OnSelectHandler: """The callback function that is invoked when a row of the DetailedList is selected.""" return self._on_select @@ -278,7 +278,7 @@ def on_select(self, handler: toga.widgets.detailedlist.OnSelectHandler) -> None: ###################################################################### @property - def on_delete(self) -> WrappedHandlerT: + def on_delete(self) -> OnPrimaryActionHandler: """**DEPRECATED**; Use :any:`on_primary_action`""" warnings.warn( "DetailedList.on_delete has been renamed DetailedList.on_primary_action.", diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 43b7a82b17..db467e9220 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -4,7 +4,7 @@ from typing import Any, Protocol import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -234,7 +234,7 @@ def pins(self) -> MapPinSet: return self._pins @property - def on_select(self) -> WrappedHandlerT: + def on_select(self) -> OnSelectHandler: """The handler to invoke when the user selects a pin on a map. **Note:** This is not currently supported on GTK or Windows. diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index f661bfc7fc..b0d19e9096 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -3,7 +3,7 @@ from typing import Any, Protocol import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -105,7 +105,7 @@ def scroll_to_top(self) -> None: self._impl.scroll_to_top() @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """The handler to invoke when the value of the widget changes.""" return self._on_change diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 92a3fe8055..5fb592a4ca 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -6,7 +6,7 @@ from typing import Any, Protocol, Union import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.types import TypeAlias from .base import StyleT, Widget @@ -295,7 +295,7 @@ def value(self, value: NumberInputT | None) -> None: self.refresh() @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """The handler to invoke when the value of the widget changes.""" return self._on_change diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 82976d4bfb..9b1b6dd237 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -9,7 +9,7 @@ ) import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.platform import get_platform_factory from toga.types import TypeAlias @@ -483,7 +483,7 @@ def window(self, window) -> None: item._content.window = window @property - def on_select(self) -> WrappedHandlerT: + def on_select(self) -> OnSelectHandler: """The callback to invoke when a new tab of content is selected.""" return self._on_select diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 4b3e43aad9..a75ce629b5 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -2,7 +2,7 @@ from typing import Any, Literal, Protocol, SupportsInt -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -130,7 +130,7 @@ def horizontal(self, value: object) -> None: self._content.refresh() @property - def on_scroll(self) -> WrappedHandlerT: + def on_scroll(self) -> OnScrollHandler: """Handler to invoke when the user moves a scroll bar.""" return self._on_scroll diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index ff55ec174f..aa249e7d3b 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -5,7 +5,7 @@ from typing import Any, Generic, Protocol, TypeVar import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.sources import ListSource, Source from .base import StyleT, Widget @@ -67,7 +67,6 @@ def __init__( ###################################################################### self._items: SourceT | ListSource[T] - self._on_change: WrappedHandlerT self.on_change = None # needed for _impl initialization self._impl = self.factory.Selection(interface=self) @@ -181,7 +180,7 @@ def value(self, value: T) -> None: raise ValueError(f"{value!r} is not a current item in the selection") @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """Handler to invoke when the value of the selection is changed, either by the user or programmatically.""" return self._on_change @@ -195,7 +194,7 @@ def on_change(self, handler: toga.widgets.selection.OnChangeHandler) -> None: ###################################################################### @property - def on_select(self) -> WrappedHandlerT: + def on_select(self) -> OnChangeHandler: """**DEPRECATED**: Use ``on_change``""" warnings.warn( "Selection.on_select has been renamed Selection.on_change.", diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index a134ca9c95..7ab7f27dbd 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -6,7 +6,7 @@ from typing import Any, Protocol, SupportsFloat import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -95,8 +95,6 @@ def __init__( # End backwards compatibility ###################################################################### - self._on_change: WrappedHandlerT - # 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 @@ -281,7 +279,7 @@ def tick_value(self, tick_value: int | None) -> None: self.value = self.min + (tick_value - 1) * self.tick_step @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """Handler to invoke when the value of the slider is changed, either by the user or programmatically. @@ -294,7 +292,7 @@ def on_change(self, handler: toga.widgets.slider.OnChangeHandler) -> None: self._on_change = wrapped_handler(self, handler) @property - def on_press(self) -> WrappedHandlerT: + def on_press(self) -> OnPressHandler: """Handler to invoke when the user presses the slider before changing it.""" return self._on_press @@ -303,7 +301,7 @@ def on_press(self, handler: toga.widgets.slider.OnPressHandler) -> None: self._on_press = wrapped_handler(self, handler) @property - def on_release(self) -> WrappedHandlerT: + def on_release(self) -> OnReleaseHandler: """Handler to invoke when the user releases the slider after changing it.""" return self._on_release diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index f4ad3511ab..cd36b3f4fe 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -3,7 +3,7 @@ from typing import Any, Protocol import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -79,7 +79,7 @@ def text(self, value: object) -> None: self.refresh() @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """The handler to invoke when the value of the switch changes.""" return self._on_change diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index e59d689542..62f9845e87 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -5,7 +5,7 @@ from typing import Any, Generic, Literal, Protocol, TypeVar import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.sources import ListSource, Row, Source from toga.sources.accessors import build_accessors, to_accessor @@ -212,7 +212,7 @@ def scroll_to_bottom(self) -> None: self.scroll_to_row(-1) @property - def on_select(self) -> WrappedHandlerT: + def on_select(self) -> OnSelectHandler: """The callback function that is invoked when a row of the table is selected.""" return self._on_select @@ -221,7 +221,7 @@ def on_select(self, handler: toga.widgets.table.OnSelectHandler) -> None: self._on_select = wrapped_handler(self, handler) @property - def on_activate(self) -> WrappedHandlerT: + def on_activate(self) -> OnActivateHandler: """The callback function that is invoked when a row of the table is activated, usually with a double click or similar action.""" return self._on_activate @@ -327,7 +327,7 @@ def missing_value(self) -> str: ###################################################################### @property - def on_double_click(self) -> WrappedHandlerT: + def on_double_click(self) -> OnActivateHandler: """**DEPRECATED**: Use ``on_activate``""" warnings.warn( "Table.on_double_click has been renamed Table.on_activate.", diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index a4fa6cbf03..01cf498ce9 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -4,7 +4,7 @@ from typing import Any, Protocol import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -158,7 +158,7 @@ def is_valid(self) -> bool: return self._impl.is_valid() @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """The handler to invoke when the value of the widget changes.""" return self._on_change @@ -186,7 +186,7 @@ def validators(self, validators: Iterable[Callable[[str], bool]] | None) -> None self._validate() @property - def on_gain_focus(self) -> WrappedHandlerT: + def on_gain_focus(self) -> OnGainFocusHandler: """The handler to invoke when the widget gains input focus.""" return self._on_gain_focus @@ -195,7 +195,7 @@ def on_gain_focus(self, handler: OnGainFocusHandler) -> None: self._on_gain_focus = wrapped_handler(self, handler) @property - def on_lose_focus(self) -> WrappedHandlerT: + def on_lose_focus(self) -> OnLoseFocusHandler: """The handler to invoke when the widget loses input focus.""" return self._on_lose_focus @@ -222,7 +222,7 @@ def _value_changed(self) -> None: self.on_change() @property - def on_confirm(self) -> WrappedHandlerT: + def on_confirm(self) -> OnConfirmHandler: """The handler to invoke when the user accepts the value of the widget, usually by pressing return/enter on the keyboard. """ diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 9dbc44e284..c9f7351498 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -5,7 +5,7 @@ from typing import Any, Protocol import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from .base import StyleT, Widget @@ -133,7 +133,7 @@ def max(self, value: object) -> None: self.value = max @property - def on_change(self) -> WrappedHandlerT: + def on_change(self) -> OnChangeHandler: """The handler to invoke when the time value changes.""" return self._on_change diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 489add9925..04f18f4702 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -5,7 +5,7 @@ from typing import Any, Generic, Literal, Protocol, TypeVar import toga -from toga.handlers import WrappedHandlerT, wrapped_handler +from toga.handlers import wrapped_handler from toga.sources import Node, Source, TreeSource from toga.sources.accessors import build_accessors, to_accessor from toga.sources.tree_source import TreeSourceDataT @@ -304,7 +304,7 @@ def missing_value(self) -> str: return self._missing_value @property - def on_select(self) -> WrappedHandlerT: + def on_select(self) -> OnSelectHandler: """The callback function that is invoked when a row of the tree is selected.""" return self._on_select @@ -313,7 +313,7 @@ def on_select(self, handler: toga.widgets.tree.OnSelectHandler) -> None: self._on_select = wrapped_handler(self, handler) @property - def on_activate(self) -> WrappedHandlerT: + def on_activate(self) -> OnActivateHandler: """The callback function that is invoked when a row of the tree is activated, usually with a double click or similar action.""" return self._on_activate @@ -327,7 +327,7 @@ def on_activate(self, handler: toga.widgets.tree.OnActivateHandler) -> None: ###################################################################### @property - def on_double_click(self) -> WrappedHandlerT: + def on_double_click(self) -> OnActivateHandler: """**DEPRECATED**: Use ``on_activate``""" warnings.warn( "Tree.on_double_click has been renamed Tree.on_activate.", diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index b1aa4d3a0f..6e397e9d78 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -4,11 +4,7 @@ from collections.abc import Callable from typing import Any, Protocol -from toga.handlers import ( - AsyncResult, - WrappedHandlerT, - wrapped_handler, -) +from toga.handlers import AsyncResult, wrapped_handler from .base import StyleT, Widget @@ -89,7 +85,7 @@ async def load_url(self, url: str) -> asyncio.Future: return await loaded_future @property - def on_webview_load(self) -> WrappedHandlerT: + def on_webview_load(self) -> OnWebViewLoadHandler: """The handler to invoke when the web view finishes loading. Rendering web content is a complex, multi-threaded process. Although a page diff --git a/core/src/toga/window.py b/core/src/toga/window.py index c2c112c4f9..72113e47fd 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -14,11 +14,7 @@ ) from toga.command import CommandSet -from toga.handlers import ( - AsyncResult, - WrappedHandlerT, - wrapped_handler, -) +from toga.handlers import AsyncResult, wrapped_handler from toga.images import Image from toga.platform import get_platform_factory @@ -475,7 +471,7 @@ def as_image(self, format: type[ImageT] = Image) -> ImageT: ###################################################################### @property - def on_close(self) -> WrappedHandlerT | None: + def on_close(self) -> OnCloseHandler | None: """The handler to invoke if the user attempts to close the window.""" return self._on_close From a1788083a440f40c8357691604ac750286898a0c Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Wed, 5 Jun 2024 12:35:57 -0400 Subject: [PATCH 09/13] Typing fixes from review --- core/pyproject.toml | 1 - core/src/toga/app.py | 6 ++--- core/src/toga/command.py | 8 ++++-- core/src/toga/handlers.py | 29 ++++++++++++--------- core/src/toga/hardware/location.py | 2 +- core/src/toga/icons.py | 6 ++++- core/src/toga/images.py | 9 ++++--- core/src/toga/sources/tree_source.py | 28 ++++++++++++-------- core/src/toga/types.py | 6 ----- core/src/toga/widgets/canvas.py | 10 +++++-- core/src/toga/widgets/dateinput.py | 3 ++- core/src/toga/widgets/detailedlist.py | 14 ++++++---- core/src/toga/widgets/mapview.py | 2 +- core/src/toga/widgets/multilinetextinput.py | 3 ++- core/src/toga/widgets/numberinput.py | 19 +++++++++----- core/src/toga/widgets/optioncontainer.py | 20 +++++++------- core/src/toga/widgets/scrollcontainer.py | 3 ++- core/src/toga/widgets/selection.py | 3 ++- core/src/toga/widgets/slider.py | 9 ++++--- core/src/toga/widgets/splitcontainer.py | 12 ++++++--- core/src/toga/widgets/switch.py | 3 ++- core/src/toga/widgets/table.py | 7 +++-- core/src/toga/widgets/textinput.py | 12 ++++++--- core/src/toga/widgets/timeinput.py | 3 ++- core/src/toga/widgets/tree.py | 6 +++-- core/src/toga/widgets/webview.py | 3 ++- core/src/toga/window.py | 2 +- 27 files changed, 143 insertions(+), 86 deletions(-) diff --git a/core/pyproject.toml b/core/pyproject.toml index 0c10c32271..3d09110f62 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -76,7 +76,6 @@ dev = [ "pytest-freezer == 0.4.8", "setuptools-scm == 8.1.0", "tox == 4.15.0", - "types-Pillow == 10.1.0.2", # TODO:PR: does this help anything? # typing-extensions needed for TypeAlias added in Py 3.10 "typing-extensions == 4.9.0 ; python_version < '3.10'", ] diff --git a/core/src/toga/app.py b/core/src/toga/app.py index b35ac27fb7..6a67c44192 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -6,7 +6,7 @@ import sys import warnings import webbrowser -from collections.abc import Iterator, Set +from collections.abc import Iterator from email.message import Message from pathlib import Path from typing import TYPE_CHECKING, Any, MutableSet, Protocol @@ -100,7 +100,7 @@ def discard(self, window: Window) -> None: # 2023-10: Backwards compatibility ###################################################################### - def __iadd__(self, window: Set[Any]) -> WindowSet: + def __iadd__(self, window: Window) -> WindowSet: # The standard set type does not have a += operator. warn( "Windows are automatically associated with the app; += is not required", @@ -109,7 +109,7 @@ def __iadd__(self, window: Set[Any]) -> WindowSet: ) return self - def __isub__(self, other: Set[Any]) -> WindowSet: + def __isub__(self, other: Window) -> WindowSet: # The standard set type does have a -= operator, but it takes sets rather than # individual items. warn( diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 257e596900..167bde400d 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -295,8 +295,12 @@ def __eq__(self, other: object) -> bool: class CommandSetChangeHandler(Protocol): - def __call__(self, /) -> object: - """A handler that will be invoked when a Command or Group is added to the CommandSet.""" + def __call__(self, **kwargs) -> object: + """A handler that will be invoked when a Command or Group is added to the + CommandSet. + + :param kwargs: Ensures compatibility with arguments added in future versions. + """ class CommandSet: diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index 5477b2b64c..64a1ea2a74 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -7,6 +7,7 @@ import warnings from abc import ABC from typing import ( + TYPE_CHECKING, Any, Awaitable, Callable, @@ -17,18 +18,22 @@ Union, ) -from toga.types import TypeAlias - -GeneratorReturnT = TypeVar("GeneratorReturnT") -HandlerGeneratorReturnT: TypeAlias = Generator[ - Union[float, None], object, GeneratorReturnT -] - -HandlerSyncT: TypeAlias = Callable[..., object] -HandlerAsyncT: TypeAlias = Callable[..., Awaitable[object]] -HandlerGeneratorT: TypeAlias = Callable[..., HandlerGeneratorReturnT[object]] -HandlerT: TypeAlias = Union[HandlerSyncT, HandlerAsyncT, HandlerGeneratorT] -WrappedHandlerT: TypeAlias = Callable[..., object] +if TYPE_CHECKING: + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias + + GeneratorReturnT = TypeVar("GeneratorReturnT") + HandlerGeneratorReturnT: TypeAlias = Generator[ + Union[float, None], object, GeneratorReturnT + ] + + HandlerSyncT: TypeAlias = Callable[..., object] + HandlerAsyncT: TypeAlias = Callable[..., Awaitable[object]] + HandlerGeneratorT: TypeAlias = Callable[..., HandlerGeneratorReturnT[object]] + HandlerT: TypeAlias = Union[HandlerSyncT, HandlerAsyncT, HandlerGeneratorT] + WrappedHandlerT: TypeAlias = Callable[..., object] class NativeHandler: diff --git a/core/src/toga/hardware/location.py b/core/src/toga/hardware/location.py index 6cfb804dff..8a23849fee 100644 --- a/core/src/toga/hardware/location.py +++ b/core/src/toga/hardware/location.py @@ -21,7 +21,7 @@ def __call__( location: toga.LatLng, altitude: float | None, **kwargs: Any, - ) -> None: + ) -> object: """A handler that will be invoked when the user's location changes. :param service: the location service that generated the update. diff --git a/core/src/toga/icons.py b/core/src/toga/icons.py index c64177cf02..810322bc54 100644 --- a/core/src/toga/icons.py +++ b/core/src/toga/icons.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import warnings from collections.abc import Callable, Iterable from pathlib import Path @@ -9,7 +10,10 @@ from toga.platform import get_platform_factory if TYPE_CHECKING: - from toga.types import TypeAlias + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias IconContent: TypeAlias = str | Path | toga.Icon diff --git a/core/src/toga/images.py b/core/src/toga/images.py index 35c78c1551..c2c3fb4f97 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -5,7 +5,7 @@ import warnings from functools import lru_cache from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Any, Protocol, TypeVar from warnings import warn import toga @@ -22,7 +22,10 @@ warnings.filterwarnings("default", category=DeprecationWarning) if TYPE_CHECKING: - from toga.types import TypeAlias, TypeVar + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias # Define a type variable for generics where an Image type is required. ImageT = TypeVar("ImageT") @@ -34,7 +37,7 @@ ImageContent: TypeAlias = PathLike | BytesLike | ImageLike # Define a type variable representing an image of an externally defined type. - ExternalImageT = TypeVar("ExternalImageT", bound=object) + ExternalImageT = TypeVar("ExternalImageT") class ImageConverter(Protocol): diff --git a/core/src/toga/sources/tree_source.py b/core/src/toga/sources/tree_source.py index 45b6828783..cf1ed6078c 100644 --- a/core/src/toga/sources/tree_source.py +++ b/core/src/toga/sources/tree_source.py @@ -1,15 +1,29 @@ from __future__ import annotations +import sys from collections.abc import Iterator -from typing import Generic, Iterable, Mapping, Tuple, TypeVar, Union - -from toga.types import TypeAlias +from typing import TYPE_CHECKING, Generic, Iterable, Mapping, Tuple, TypeVar, Union from .base import Source from .list_source import Row, _find_item T = TypeVar("T") +if TYPE_CHECKING: + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias + + NodeDataT: TypeAlias = Union[object, Mapping[str, T], Iterable[T]] + TreeSourceDataT: TypeAlias = Union[ + object, + Mapping[NodeDataT[T], "TreeSourceDataT[T]"], + Iterable[Tuple[NodeDataT[T], "TreeSourceDataT[T]"]], + ] +else: + TreeSourceDataT = None + class Node(Row[T]): _source: TreeSource[T] @@ -195,14 +209,6 @@ def find(self, data: object, start: Node[T] | None = None) -> Node[T]: ) -NodeDataT: TypeAlias = Union[object, Mapping[str, T], Iterable[T]] -TreeSourceDataT: TypeAlias = Union[ - object, - Mapping[NodeDataT[T], "TreeSourceDataT[T]"], - Iterable[Tuple[NodeDataT[T], "TreeSourceDataT[T]"]], -] - - class TreeSource(Source, Generic[T]): def __init__( self, diff --git a/core/src/toga/types.py b/core/src/toga/types.py index dc9291be25..7a864d7eea 100644 --- a/core/src/toga/types.py +++ b/core/src/toga/types.py @@ -1,13 +1,7 @@ from __future__ import annotations -import sys from typing import NamedTuple -if sys.version_info < (3, 10): - from typing_extensions import TypeAlias, TypeVar # noqa:F401 -else: - from typing import TypeAlias, TypeVar # noqa:F401 - class LatLng(NamedTuple): """A geographic coordinate.""" diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 83e55b3692..b667bc1721 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1174,7 +1174,7 @@ def color(self, value: object) -> None: class OnTouchHandler(Protocol): - def __call__(self, widget: Canvas, x: int, y: int, **kwargs: Any) -> None: + def __call__(self, widget: Canvas, x: int, y: int, **kwargs: Any) -> object: """A handler that will be invoked when a :any:`Canvas` is touched with a finger or mouse. @@ -1186,7 +1186,13 @@ def __call__(self, widget: Canvas, x: int, y: int, **kwargs: Any) -> None: class OnResizeHandler(Protocol): - def __call__(self, widget: Canvas, width: int, height: int, **kwargs: Any) -> None: + def __call__( + self, + widget: Canvas, + width: int, + height: int, + **kwargs: Any, + ) -> object: """A handler that will be invoked when a :any:`Canvas` is resized. :param widget: The canvas that was resized. diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index e4e73d2bb4..2fae149de1 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -18,9 +18,10 @@ class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> None: + def __call__(self, widget: DateInput, **kwargs: Any) -> object: """A handler that will be invoked when a change occurs. + :param widget: The DateInput that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 5f19ee64ad..afcee2a820 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -21,35 +21,39 @@ class OnPrimaryActionHandler(Protocol): - def __call__(self, row: Any, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, row: Any, **kwargs: Any) -> object: """A handler to invoke for the primary action. - :param widget: The button that was pressed. + :param widget: The DetailedList that was invoked. + :param row: The current row for the detailed list. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnSecondaryActionHandler(Protocol): - def __call__(self, row: Any, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, row: Any, **kwargs: Any) -> object: """A handler to invoke for the secondary action. + :param widget: The DetailedList that was invoked. :param row: The current row for the detailed list. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnRefreshHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, **kwargs: Any) -> object: """A handler to invoke when the detailed list is refreshed. + :param widget: The DetailedList that was refreshed. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnSelectHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, **kwargs: Any) -> object: """A handler to invoke when the detailed list is selected. + :param widget: The DetailedList that was selected. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index db467e9220..810b174acf 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -125,7 +125,7 @@ class OnSelectHandler(Protocol): def __call__(self, widget: MapView, *, pin: MapPin, **kwargs: Any) -> object: """A handler that will be invoked when the user selects a map pin. - :param widget: The button that was pressed. + :param widget: The MapView that was selected. :param pin: The pin that was selected. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index b0d19e9096..53fe7c5faf 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -9,9 +9,10 @@ class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: MultilineTextInput, **kwargs: Any) -> object: """A handler to invoke when the value is changed. + :param widget: The MultilineTextInput that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 5fb592a4ca..2fef142af5 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -1,16 +1,25 @@ from __future__ import annotations import re +import sys import warnings from decimal import ROUND_HALF_UP, Decimal, InvalidOperation -from typing import Any, Protocol, Union +from typing import TYPE_CHECKING, Any, Protocol, Union import toga from toga.handlers import wrapped_handler -from toga.types import TypeAlias from .base import StyleT, Widget +if TYPE_CHECKING: + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias + + NumberInputT: TypeAlias = Union[Decimal, float, str] + StepInputT: TypeAlias = Union[Decimal, int] + # Implementation notes # ==================== # @@ -25,9 +34,6 @@ NUMERIC_RE = re.compile(r"[^0-9\.-]") -NumberInputT: TypeAlias = Union[Decimal, float, str] -StepInputT: TypeAlias = Union[Decimal, int] - def _clean_decimal(value: NumberInputT, step: StepInputT | None = None) -> Decimal: # Decimal(3.7) yields "3.700000000...177". @@ -68,9 +74,10 @@ def _clean_decimal_str(value: str) -> str: class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: NumberInput, **kwargs: Any) -> object: """A handler to invoke when the value is changed. + :param widget: The NumberInput that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 9b1b6dd237..a3666d0ce3 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -1,21 +1,20 @@ from __future__ import annotations +import sys from collections.abc import Iterable -from typing import ( - TYPE_CHECKING, - Any, - Protocol, - overload, -) +from typing import TYPE_CHECKING, Any, Protocol, overload import toga from toga.handlers import wrapped_handler from toga.platform import get_platform_factory -from toga.types import TypeAlias from .base import StyleT, Widget if TYPE_CHECKING: + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias from toga.icons import IconContent OptionContainerContent: TypeAlias = ( @@ -27,9 +26,10 @@ class OnSelectHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: - """A handler to invoke when the option list is selected. + def __call__(self, widget: OptionContainer, **kwargs: Any) -> None: + """A handler that will be invoked when a new tab is selected in the OptionContainer. + :param widget: The OptionContainer that had a selection change. :param kwargs: Ensures compatibility with arguments added in future versions. """ @@ -380,7 +380,7 @@ def __init__( self, id: str | None = None, style: StyleT | None = None, - content: Iterable[tuple[str, Widget]] | None = None, + content: Iterable[OptionContainerContent] | None = None, on_select: toga.widgets.optioncontainer.OnSelectHandler | None = None, ): """Create a new OptionContainer. diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index a75ce629b5..aa82f14c0b 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -8,9 +8,10 @@ class OnScrollHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: ScrollContainer, **kwargs: Any) -> object: """A handler to invoke when the container is scrolled. + :param widget: The ScrollContainer that was scrolled. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index aa249e7d3b..7a29aa7ea2 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -15,9 +15,10 @@ class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: Selection, **kwargs: Any) -> object: """A handler to invoke when the value is changed. + :param widget: The Selection that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 7ab7f27dbd..5d91513dbe 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -12,25 +12,28 @@ class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: Slider, **kwargs: Any) -> object: """A handler to invoke when the value is changed. + :param widget: The Slider that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnPressHandler(Protocol): - def __call__(self, **kwargs) -> object: + def __call__(self, widget: Slider, **kwargs) -> object: """A handler to invoke when the slider is pressed. + :param widget: The Slider that was pressed. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnReleaseHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: Slider, **kwargs: Any) -> object: """A handler to invoke when the slider is pressed. + :param widget: The Slider that was released. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 3ef8050f5a..7dc120d667 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -1,15 +1,21 @@ from __future__ import annotations -from typing import Tuple, Union +import sys +from typing import TYPE_CHECKING, Tuple, Union from toga.app import App from toga.constants import Direction -from toga.types import TypeAlias from toga.window import Window from .base import StyleT, Widget -ContentT: TypeAlias = Union[Widget, Tuple[Widget, float], None] +if TYPE_CHECKING: + if sys.version_info < (3, 10): + from typing_extensions import TypeAlias + else: + from typing import TypeAlias + + ContentT: TypeAlias = Union[Widget, Tuple[Widget, float], None] class SplitContainer(Widget): diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index cd36b3f4fe..0ec3a0b95e 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -9,9 +9,10 @@ class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: Switch, **kwargs: Any) -> object: """A handler to invoke when the value is changed. + :param widget: The Switch that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index 62f9845e87..e0b6df2262 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -16,17 +16,20 @@ class OnSelectHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: Table, **kwargs: Any) -> object: """A handler to invoke when the table is selected. + :param widget: The Table that was selected. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnActivateHandler(Protocol): - def __call__(self, row: Any, **kwargs: Any) -> object: + def __call__(self, widget: Table, row: Any, **kwargs: Any) -> object: """A handler to invoke when the table is activated. + :param widget: The Table that was activated. + :param row: The Table Row that was activated. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 01cf498ce9..0302970b9d 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -10,33 +10,37 @@ class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, **kwargs: Any) -> object: """A handler to invoke when the text input is changed. + :param widget: The TextInput that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnConfirmHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, **kwargs: Any) -> object: """A handler to invoke when the text input is confirmed. + :param widget: The TextInput that was confirmed. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnGainFocusHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, **kwargs: Any) -> object: """A handler to invoke when the text input gains focus. + :param widget: The TextInput that gained focus. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnLoseFocusHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, **kwargs: Any) -> object: """A handler to invoke when the text input loses focus. + :param widget: The TextInput that lost focus. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index c9f7351498..530747e27e 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -11,9 +11,10 @@ class OnChangeHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: TimeInput, **kwargs: Any) -> object: """A handler to invoke when the time input is changed. + :param widget: The TimeInput that was changed. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 04f18f4702..560f35fdaf 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -18,17 +18,19 @@ class OnSelectHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: Tree, **kwargs: Any) -> object: """A handler to invoke when the tree is selected. + :param widget: The Tree that was selected. :param kwargs: Ensures compatibility with arguments added in future versions. """ class OnActivateHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: Tree, **kwargs: Any) -> object: """A handler to invoke when the tree is activated. + :param widget: The Tree that was activated. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 6e397e9d78..5b15857206 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -14,9 +14,10 @@ class JavaScriptResult(AsyncResult): class OnWebViewLoadHandler(Protocol): - def __call__(self, **kwargs: Any) -> object: + def __call__(self, widget: WebView, **kwargs: Any) -> object: """A handler to invoke when the WebView is loaded. + :param widget: The WebView that was loaded. :param kwargs: Ensures compatibility with arguments added in future versions. """ diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 72113e47fd..bdc47f5490 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -88,7 +88,7 @@ def __call__(self, window: Window, **kwargs: Any) -> bool: class DialogResultHandler(Protocol[_DialogResultT]): - def __call__(self, window: Window, result: _DialogResultT, **kwargs: Any) -> Any: + def __call__(self, window: Window, result: _DialogResultT, **kwargs: Any) -> object: """A handler to invoke when a dialog is closed. :param window: The window that opened the dialog. From 38b56c61f1e34e355a2885602fd3feee5c8c095b Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Fri, 7 Jun 2024 13:38:22 -0400 Subject: [PATCH 10/13] Revert attempt to make `Source`s generic --- core/src/toga/sources/accessors.py | 2 +- core/src/toga/sources/base.py | 10 ++-- core/src/toga/sources/list_source.py | 25 +++++---- core/src/toga/sources/tree_source.py | 76 ++++++++------------------- core/src/toga/widgets/detailedlist.py | 21 +++----- core/src/toga/widgets/selection.py | 19 ++++--- core/src/toga/widgets/table.py | 15 +++--- core/src/toga/widgets/tree.py | 20 ++++--- 8 files changed, 76 insertions(+), 112 deletions(-) diff --git a/core/src/toga/sources/accessors.py b/core/src/toga/sources/accessors.py index d9fc673711..e5f2194cd7 100644 --- a/core/src/toga/sources/accessors.py +++ b/core/src/toga/sources/accessors.py @@ -63,7 +63,7 @@ def build_accessors( :returns: The final list of accessors. """ - if accessors: + if accessors is not None: if isinstance(accessors, Mapping): result = [ accessors[h] if h in accessors else to_accessor(h) for h in headings diff --git a/core/src/toga/sources/base.py b/core/src/toga/sources/base.py index 4f730e0f84..8ea13d2d5b 100644 --- a/core/src/toga/sources/base.py +++ b/core/src/toga/sources/base.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Protocol +from typing import Protocol class Listener(Protocol): @@ -8,27 +8,27 @@ class Listener(Protocol): data source. """ - def change(self, item: Any) -> None: + def change(self, item: object) -> object: """A change has occurred in an item. :param item: The data object that has changed. """ - def insert(self, index: int, item: Any) -> None: + def insert(self, index: int, item: object) -> object: """An item has been added to the data source. :param index: The 0-index position in the data. :param item: The data object that was added. """ - def remove(self, index: int, item: Any) -> None: + def remove(self, index: int, item: object) -> object: """An item has been removed from the data source. :param index: The 0-index position in the data. :param item: The data object that was added. """ - def clear(self) -> None: + def clear(self) -> object: """All items have been removed from the data source.""" diff --git a/core/src/toga/sources/list_source.py b/core/src/toga/sources/list_source.py index 873b992a06..f64aeaa7d7 100644 --- a/core/src/toga/sources/list_source.py +++ b/core/src/toga/sources/list_source.py @@ -82,6 +82,9 @@ def __setattr__(self, attr: str, value: T) -> None: if self._source is not None: self._source.notify("change", item=self) + def __getattr__(self, attr: str) -> T: + return super().__getattr__(attr) + def __delattr__(self, attr: str) -> None: """Remove an attribute from the Row object, notifying the source of the change. @@ -94,8 +97,10 @@ def __delattr__(self, attr: str) -> None: # TODO:PR: consider adding supported Protocols...maybe List? -class ListSource(Source, Generic[T]): - def __init__(self, accessors: Iterable[str], data: Iterable[T] | None = None): +class ListSource(Source): + _data: list[Row] + + def __init__(self, accessors: Iterable[str], data: Iterable | None = None): """A data source to store an ordered list of multiple data values. :param accessors: A list of attribute names for accessing the value @@ -126,7 +131,7 @@ def __len__(self) -> int: """Returns the number of items in the list.""" return len(self._data) - def __getitem__(self, index: int) -> Row[T]: + def __getitem__(self, index: int) -> Row: """Returns the item at position ``index`` of the list.""" return self._data[index] @@ -141,7 +146,7 @@ def __delitem__(self, index: int) -> None: ###################################################################### # This behavior is documented in list_source.rst. - def _create_row(self, data: T) -> Row[T]: + def _create_row(self, data: object) -> Row: if isinstance(data, Mapping): row = Row(**data) elif hasattr(data, "__iter__") and not isinstance(data, str): @@ -155,7 +160,7 @@ def _create_row(self, data: T) -> Row[T]: # Utility methods to make ListSources more list-like ###################################################################### - def __setitem__(self, index: int, value: T) -> None: + def __setitem__(self, index: int, value: object) -> None: """Set the value of a specific item in the data source. :param index: The item to change @@ -171,7 +176,7 @@ def clear(self) -> None: self._data = [] self.notify("clear") - def insert(self, index: int, data: T) -> Row[T]: + def insert(self, index: int, data: object) -> Row: """Insert a row into the data source at a specific index. :param index: The index at which to insert the item. @@ -184,7 +189,7 @@ def insert(self, index: int, data: T) -> Row[T]: self.notify("insert", index=index, item=row) return row - def append(self, data: T) -> Row[T]: + def append(self, data: object) -> Row: """Insert a row at the end of the data source. :param data: The data to append to the ListSource. This data will be converted @@ -193,14 +198,14 @@ def append(self, data: T) -> Row[T]: """ return self.insert(len(self), data) - def remove(self, row: Row[T]) -> None: + def remove(self, row: Row) -> None: """Remove a row from the data source. :param row: The row to remove from the data source. """ del self[self._data.index(row)] - def index(self, row: Row[T]) -> int: + def index(self, row: Row) -> int: """The index of a specific row in the data source. This search uses Row instances, and searches for an *instance* match. @@ -214,7 +219,7 @@ def index(self, row: Row[T]) -> int: """ return self._data.index(row) - def find(self, data: object, start: Row[T] | None = None) -> Row[T]: + def find(self, data: object, start: Row | None = None) -> Row: """Find the first item in the data that matches all the provided attributes. diff --git a/core/src/toga/sources/tree_source.py b/core/src/toga/sources/tree_source.py index cf1ed6078c..967a97267e 100644 --- a/core/src/toga/sources/tree_source.py +++ b/core/src/toga/sources/tree_source.py @@ -1,32 +1,16 @@ from __future__ import annotations -import sys from collections.abc import Iterator -from typing import TYPE_CHECKING, Generic, Iterable, Mapping, Tuple, TypeVar, Union +from typing import Iterable, Mapping, TypeVar from .base import Source from .list_source import Row, _find_item T = TypeVar("T") -if TYPE_CHECKING: - if sys.version_info < (3, 10): - from typing_extensions import TypeAlias - else: - from typing import TypeAlias - - NodeDataT: TypeAlias = Union[object, Mapping[str, T], Iterable[T]] - TreeSourceDataT: TypeAlias = Union[ - object, - Mapping[NodeDataT[T], "TreeSourceDataT[T]"], - Iterable[Tuple[NodeDataT[T], "TreeSourceDataT[T]"]], - ] -else: - TreeSourceDataT = None - class Node(Row[T]): - _source: TreeSource[T] + _source: TreeSource def __init__(self, **data: T): """Create a new Node object. @@ -82,7 +66,7 @@ def __delitem__(self, index: int) -> None: self._source.notify("remove", parent=self, index=index, item=child) def __len__(self) -> int: - if self._children is not None: + if self.can_have_children(): return len(self._children) else: return 0 @@ -103,7 +87,7 @@ def can_have_children(self) -> bool: def __iter__(self) -> Iterator[Node[T]]: return iter(self._children or []) - def __setitem__(self, index: int, data: T) -> None: + def __setitem__(self, index: int, data: object) -> None: """Set the value of a specific child in the Node. :param index: The index of the child to change @@ -209,12 +193,10 @@ def find(self, data: object, start: Node[T] | None = None) -> Node[T]: ) -class TreeSource(Source, Generic[T]): - def __init__( - self, - accessors: Iterable[str], - data: TreeSourceDataT[T] | None = None, - ): +class TreeSource(Source): + _roots: list[Node] + + def __init__(self, accessors: Iterable[str], data: object | None = None): super().__init__() if isinstance(accessors, str) or not hasattr(accessors, "__iter__"): raise ValueError("accessors should be a list of attribute names") @@ -225,7 +207,7 @@ def __init__( raise ValueError("TreeSource must be provided a list of accessors") if data is not None: - self._roots: list[Node[T]] = self._create_nodes(parent=None, value=data) + self._roots = self._create_nodes(parent=None, value=data) else: self._roots = [] @@ -236,7 +218,7 @@ def __init__( def __len__(self) -> int: return len(self._roots) - def __getitem__(self, index: int) -> Node[T]: + def __getitem__(self, index: int) -> Node: return self._roots[index] def __delitem__(self, index: int) -> None: @@ -251,11 +233,11 @@ def __delitem__(self, index: int) -> None: def _create_node( self, - parent: Node[T] | None, - data: NodeDataT[T], - children: TreeSourceDataT[T] | None = None, - ) -> Node[T]: - if isinstance(data, dict): + parent: Node | None, + data: object, + children: object | None = None, + ) -> Node: + if isinstance(data, Mapping): node = Node(**data) elif hasattr(data, "__iter__") and not isinstance(data, str): node = Node(**dict(zip(self._accessors, data))) @@ -270,12 +252,8 @@ def _create_node( return node - def _create_nodes( - self, - parent: Node[T] | None, - value: TreeSourceDataT[T], - ) -> list[Node[T]]: - if isinstance(value, dict): + def _create_nodes(self, parent: Node | None, value: object) -> list[Node]: + if isinstance(value, Mapping): return [ self._create_node(parent=parent, data=data, children=children) for data, children in value.items() @@ -312,12 +290,7 @@ def clear(self) -> None: self._roots = [] self.notify("clear") - def insert( - self, - index: int, - data: NodeDataT[T], - children: TreeSourceDataT[T] = None, - ) -> Node[T]: + def insert(self, index: int, data: object, children: object = None) -> Node: """Insert a root node into the data source at a specific index. If the node is a leaf node, it will be converted into a non-leaf node. @@ -338,13 +311,10 @@ def insert( self._roots.insert(index, node) node._parent = None self.notify("insert", parent=None, index=index, item=node) + return node - def append( - self, - data: NodeDataT[T], - children: TreeSourceDataT[T] | None = None, - ) -> Node[T]: + def append(self, data: object, children: object | None = None) -> Node: """Append a root node at the end of the list of children of this source. If the node is a leaf node, it will be converted into a non-leaf node. @@ -357,7 +327,7 @@ def append( """ return self.insert(len(self), data=data, children=children) - def remove(self, node: Node[T]) -> None: + def remove(self, node: Node) -> None: """Remove a node from the data source. This will also remove the node if it is a descendant of a root node. @@ -372,7 +342,7 @@ def remove(self, node: Node[T]) -> None: else: node._parent.remove(node) - def index(self, node: Node[T]) -> int: + def index(self, node: Node) -> int: """The index of a specific root node in the data source. This search uses Node instances, and searches for an *instance* match. @@ -386,7 +356,7 @@ def index(self, node: Node[T]) -> int: """ return self._roots.index(node) - def find(self, data: NodeDataT[T], start: Node[T] | None = None) -> Node[T]: + def find(self, data: object, start: Node | None = None) -> Node: """Find the first item in the child nodes of the given node that matches all the provided attributes. diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index afcee2a820..0da3687a98 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -2,13 +2,7 @@ import warnings from collections.abc import Iterable -from typing import ( - Any, - Generic, - Literal, - Protocol, - TypeVar, -) +from typing import Any, Literal, Protocol, TypeVar import toga from toga.handlers import wrapped_handler @@ -16,7 +10,6 @@ from .base import StyleT, Widget -T = TypeVar("T") SourceT = TypeVar("SourceT", bound=Source) @@ -58,12 +51,12 @@ def __call__(self, widget: DetailedList, **kwargs: Any) -> object: """ -class DetailedList(Widget, Generic[T]): +class DetailedList(Widget): def __init__( self, id: str | None = None, style: StyleT | None = None, - data: SourceT | Iterable[T] | None = None, + data: SourceT | Iterable | None = None, accessors: tuple[str, str, str] = ("title", "subtitle", "icon"), missing_value: str = "", primary_action: str | None = "Delete", @@ -118,7 +111,7 @@ def __init__( self.on_select = None # TODO:PR: in reality, _data needs to be Sized and SupportsIndex... - self._data: SourceT | ListSource[T] = None + self._data: SourceT | ListSource = None self._impl = self.factory.DetailedList(interface=self) @@ -145,7 +138,7 @@ def focus(self) -> None: pass @property - def data(self) -> SourceT | ListSource[T]: + def data(self) -> SourceT | ListSource: """The data to display in the table. When setting this property: @@ -161,7 +154,7 @@ def data(self) -> SourceT | ListSource[T]: return self._data @data.setter - def data(self, data: SourceT | Iterable[T] | None) -> None: + def data(self, data: SourceT | Iterable | None) -> None: if data is None: self._data = ListSource(data=[], accessors=self.accessors) elif isinstance(data, Source): @@ -205,7 +198,7 @@ def missing_value(self) -> str: return self._missing_value @property - def selection(self) -> Row[T] | None: + def selection(self) -> Row | None: """The current selection of the table. Returns the selected Row object, or :any:`None` if no row is currently selected. diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 7a29aa7ea2..2727b9bd41 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -2,7 +2,7 @@ import warnings from collections.abc import Iterable -from typing import Any, Generic, Protocol, TypeVar +from typing import Any, Protocol, TypeVar import toga from toga.handlers import wrapped_handler @@ -10,7 +10,6 @@ from .base import StyleT, Widget -T = TypeVar("T") SourceT = TypeVar("SourceT", bound=Source) @@ -23,14 +22,14 @@ def __call__(self, widget: Selection, **kwargs: Any) -> object: """ -class Selection(Widget, Generic[T]): +class Selection(Widget): def __init__( self, id: str | None = None, style: StyleT | None = None, - items: SourceT | Iterable[T] | None = None, + items: SourceT | Iterable | None = None, accessor: str | None = None, - value: T | None = None, + value: object | None = None, on_change: toga.widgets.selection.OnChangeHandler | None = None, enabled: bool = True, on_select: None = None, # DEPRECATED @@ -67,7 +66,7 @@ def __init__( # End backwards compatibility. ###################################################################### - self._items: SourceT | ListSource[T] + self._items: SourceT | ListSource self.on_change = None # needed for _impl initialization self._impl = self.factory.Selection(interface=self) @@ -81,7 +80,7 @@ def __init__( self.enabled = enabled @property - def items(self) -> SourceT | ListSource[T]: + def items(self) -> SourceT | ListSource: """The items to display in the selection. When setting this property: @@ -98,7 +97,7 @@ def items(self) -> SourceT | ListSource[T]: return self._items @items.setter - def items(self, items: SourceT | Iterable[T] | None) -> None: + def items(self, items: SourceT | Iterable | None) -> None: if self._accessor is None: accessors = ["value"] else: @@ -140,7 +139,7 @@ def _title_for_item(self, item: Any) -> str: return str(title).split("\n")[0] @property - def value(self) -> T | None: + def value(self) -> object | None: """The currently selected item. Returns None if there are no items in the selection. @@ -168,7 +167,7 @@ def value(self) -> T | None: return item @value.setter - def value(self, value: T) -> None: + def value(self, value: object) -> None: try: if self._accessor is None: item = self._items.find(dict(value=value)) diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index e0b6df2262..b35f6b3d19 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -2,7 +2,7 @@ import warnings from collections.abc import Iterable -from typing import Any, Generic, Literal, Protocol, TypeVar +from typing import Any, Literal, Protocol, TypeVar import toga from toga.handlers import wrapped_handler @@ -11,7 +11,6 @@ from .base import StyleT, Widget -T = TypeVar("T") SourceT = TypeVar("SourceT", bound=Source) @@ -34,13 +33,13 @@ def __call__(self, widget: Table, row: Any, **kwargs: Any) -> object: """ -class Table(Widget, Generic[T]): +class Table(Widget): def __init__( self, headings: Iterable[str] | None = None, id: str | None = None, style: StyleT | None = None, - data: SourceT | Iterable[T] | None = None, + data: SourceT | Iterable | None = None, accessors: Iterable[str] | None = None, multiple_select: bool = False, on_select: toga.widgets.table.OnSelectHandler | None = None, @@ -99,7 +98,7 @@ def __init__( self._headings: list[str] | None self._accessors: list[str] - self._data: SourceT | ListSource[T] + self._data: SourceT | ListSource if headings is not None: self._headings = [heading.split("\n")[0] for heading in headings] @@ -143,7 +142,7 @@ def focus(self) -> None: pass @property - def data(self) -> ListSource[T]: + def data(self) -> SourceT | ListSource: """The data to display in the table. When setting this property: @@ -159,7 +158,7 @@ def data(self) -> ListSource[T]: return self._data @data.setter - def data(self, data: SourceT | Iterable[T] | None) -> None: + def data(self, data: SourceT | Iterable | None) -> None: if data is None: self._data = ListSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): @@ -176,7 +175,7 @@ def multiple_select(self) -> bool: return self._multiple_select @property - def selection(self) -> list[Row[T]] | Row[T] | None: + def selection(self) -> list[Row] | Row | None: """The current selection of the table. If multiple selection is enabled, returns a list of Row objects from the data diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 560f35fdaf..5809f359e5 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -2,18 +2,16 @@ import warnings from collections.abc import Iterable -from typing import Any, Generic, Literal, Protocol, TypeVar +from typing import Any, Literal, Protocol, TypeVar import toga from toga.handlers import wrapped_handler from toga.sources import Node, Source, TreeSource from toga.sources.accessors import build_accessors, to_accessor -from toga.sources.tree_source import TreeSourceDataT from toga.style import Pack from .base import Widget -T = TypeVar("T") SourceT = TypeVar("SourceT", bound=Source) @@ -35,13 +33,13 @@ def __call__(self, widget: Tree, **kwargs: Any) -> object: """ -class Tree(Widget, Generic[T]): +class Tree(Widget): def __init__( self, headings: Iterable[str] | None = None, id: str | None = None, style: Pack | None = None, - data: SourceT | TreeSourceDataT[T] | None = None, + data: SourceT | object | None = None, accessors: Iterable[str] | None = None, multiple_select: bool = False, on_select: toga.widgets.tree.OnSelectHandler | None = None, @@ -99,7 +97,7 @@ def __init__( ###################################################################### self._headings: list[str] | None - self._data: SourceT | TreeSource[T] + self._data: SourceT | TreeSource if headings is not None: self._headings = [heading.split("\n")[0] for heading in headings] @@ -142,7 +140,7 @@ def focus(self) -> None: pass @property - def data(self) -> TreeSource[T]: + def data(self) -> SourceT | TreeSource: """The data to display in the tree. When setting this property: @@ -158,7 +156,7 @@ def data(self) -> TreeSource[T]: return self._data @data.setter - def data(self, data: SourceT | TreeSourceDataT[T] | None) -> None: + def data(self, data: SourceT | object | None) -> None: if data is None: self._data = TreeSource(accessors=self._accessors, data=[]) elif isinstance(data, Source): @@ -175,7 +173,7 @@ def multiple_select(self) -> bool: return self._multiple_select @property - def selection(self) -> list[Node[T]] | Node[T] | None: + def selection(self) -> list[Node] | Node | None: """The current selection of the tree. If multiple selection is enabled, returns a list of Node objects from the data @@ -187,7 +185,7 @@ def selection(self) -> list[Node[T]] | Node[T] | None: """ return self._impl.get_selection() - def expand(self, node: Node[T] | None = None) -> None: + def expand(self, node: Node | None = None) -> None: """Expand the specified node of the tree. If no node is provided, all nodes of the tree will be expanded. @@ -204,7 +202,7 @@ def expand(self, node: Node[T] | None = None) -> None: else: self._impl.expand_node(node) - def collapse(self, node: Node[T] | None = None) -> None: + def collapse(self, node: Node | None = None) -> None: """Collapse the specified node of the tree. If no node is provided, all nodes of the tree will be collapsed. From 56356fc46560ebee88f768c9cf8476e586c660de Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Fri, 7 Jun 2024 14:17:31 -0400 Subject: [PATCH 11/13] Don't enforce named positional arguments in handlers --- core/src/toga/app.py | 6 +++--- core/src/toga/hardware/location.py | 1 + core/src/toga/widgets/button.py | 2 +- core/src/toga/widgets/canvas.py | 8 ++------ core/src/toga/widgets/dateinput.py | 2 +- core/src/toga/widgets/detailedlist.py | 8 ++++---- 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/scrollcontainer.py | 2 +- core/src/toga/widgets/selection.py | 2 +- core/src/toga/widgets/slider.py | 6 +++--- core/src/toga/widgets/switch.py | 2 +- core/src/toga/widgets/table.py | 4 ++-- core/src/toga/widgets/textinput.py | 8 ++++---- core/src/toga/widgets/timeinput.py | 2 +- core/src/toga/widgets/tree.py | 4 ++-- core/src/toga/widgets/webview.py | 7 +++---- core/src/toga/window.py | 8 +++++--- 20 files changed, 39 insertions(+), 41 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 6a67c44192..61ee05581f 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -33,7 +33,7 @@ class AppStartupMethod(Protocol): - def __call__(self, app: App, **kwargs: Any) -> Widget: + def __call__(self, app: App, /, **kwargs: Any) -> Widget: """The startup method of the app. Called during app startup to set the initial main window content. @@ -46,7 +46,7 @@ def __call__(self, app: App, **kwargs: Any) -> Widget: class OnExitHandler(Protocol): - def __call__(self, app: App, **kwargs: Any) -> bool: + def __call__(self, app: App, /, **kwargs: Any) -> bool: """A handler to invoke when the app is about to exit. The return value of this callback controls whether the app is allowed to exit. @@ -61,7 +61,7 @@ def __call__(self, app: App, **kwargs: Any) -> bool: class BackgroundTask(Protocol): - def __call__(self, app: App, **kwargs: Any) -> object: + def __call__(self, app: App, /, **kwargs: Any) -> object: """Code that should be executed as a background task. :param app: The app that is handling the background task. diff --git a/core/src/toga/hardware/location.py b/core/src/toga/hardware/location.py index 8a23849fee..56f9801c0e 100644 --- a/core/src/toga/hardware/location.py +++ b/core/src/toga/hardware/location.py @@ -17,6 +17,7 @@ class LocationResult(AsyncResult): class OnLocationChangeHandler(Protocol): def __call__( self, + *, service: Location, location: toga.LatLng, altitude: float | None, diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index f18b4db11f..6f34389d8a 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -12,7 +12,7 @@ class OnPressHandler(Protocol): - def __call__(self, widget: Button, **kwargs: Any) -> object: + def __call__(self, widget: Button, /, **kwargs: Any) -> object: """A handler that will be invoked when a button is pressed. :param widget: The button that was pressed. diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index b667bc1721..8adf19c1aa 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1174,7 +1174,7 @@ def color(self, value: object) -> None: class OnTouchHandler(Protocol): - def __call__(self, widget: Canvas, x: int, y: int, **kwargs: Any) -> object: + def __call__(self, widget: Canvas, x: int, y: int, /, **kwargs: Any) -> object: """A handler that will be invoked when a :any:`Canvas` is touched with a finger or mouse. @@ -1187,11 +1187,7 @@ def __call__(self, widget: Canvas, x: int, y: int, **kwargs: Any) -> object: class OnResizeHandler(Protocol): def __call__( - self, - widget: Canvas, - width: int, - height: int, - **kwargs: Any, + self, widget: Canvas, /, width: int, height: int, **kwargs: Any ) -> object: """A handler that will be invoked when a :any:`Canvas` is resized. diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 2fae149de1..7408f38ae2 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -18,7 +18,7 @@ class OnChangeHandler(Protocol): - def __call__(self, widget: DateInput, **kwargs: Any) -> object: + def __call__(self, widget: DateInput, /, **kwargs: Any) -> object: """A handler that will be invoked when a change occurs. :param widget: The DateInput that was changed. diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 0da3687a98..676f0e1361 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -14,7 +14,7 @@ class OnPrimaryActionHandler(Protocol): - def __call__(self, widget: DetailedList, row: Any, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, row: Any, /, **kwargs: Any) -> object: """A handler to invoke for the primary action. :param widget: The DetailedList that was invoked. @@ -24,7 +24,7 @@ def __call__(self, widget: DetailedList, row: Any, **kwargs: Any) -> object: class OnSecondaryActionHandler(Protocol): - def __call__(self, widget: DetailedList, row: Any, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, row: Any, /, **kwargs: Any) -> object: """A handler to invoke for the secondary action. :param widget: The DetailedList that was invoked. @@ -34,7 +34,7 @@ def __call__(self, widget: DetailedList, row: Any, **kwargs: Any) -> object: class OnRefreshHandler(Protocol): - def __call__(self, widget: DetailedList, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, /, **kwargs: Any) -> object: """A handler to invoke when the detailed list is refreshed. :param widget: The DetailedList that was refreshed. @@ -43,7 +43,7 @@ def __call__(self, widget: DetailedList, **kwargs: Any) -> object: class OnSelectHandler(Protocol): - def __call__(self, widget: DetailedList, **kwargs: Any) -> object: + def __call__(self, widget: DetailedList, /, **kwargs: Any) -> object: """A handler to invoke when the detailed list is selected. :param widget: The DetailedList that was selected. diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 810b174acf..0ba7b8ab40 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -122,7 +122,7 @@ def clear(self) -> None: class OnSelectHandler(Protocol): - def __call__(self, widget: MapView, *, pin: MapPin, **kwargs: Any) -> object: + def __call__(self, widget: MapView, /, *, pin: MapPin, **kwargs: Any) -> object: """A handler that will be invoked when the user selects a map pin. :param widget: The MapView that was selected. diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 53fe7c5faf..d987725c35 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -9,7 +9,7 @@ class OnChangeHandler(Protocol): - def __call__(self, widget: MultilineTextInput, **kwargs: Any) -> object: + def __call__(self, widget: MultilineTextInput, /, **kwargs: Any) -> object: """A handler to invoke when the value is changed. :param widget: The MultilineTextInput that was changed. diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 2fef142af5..b50f556219 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -74,7 +74,7 @@ def _clean_decimal_str(value: str) -> str: class OnChangeHandler(Protocol): - def __call__(self, widget: NumberInput, **kwargs: Any) -> object: + def __call__(self, widget: NumberInput, /, **kwargs: Any) -> object: """A handler to invoke when the value is changed. :param widget: The NumberInput that was changed. diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index a3666d0ce3..5de0337fd1 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -26,7 +26,7 @@ class OnSelectHandler(Protocol): - def __call__(self, widget: OptionContainer, **kwargs: Any) -> None: + def __call__(self, widget: OptionContainer, /, **kwargs: Any) -> None: """A handler that will be invoked when a new tab is selected in the OptionContainer. :param widget: The OptionContainer that had a selection change. diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index aa82f14c0b..063698f2c2 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -8,7 +8,7 @@ class OnScrollHandler(Protocol): - def __call__(self, widget: ScrollContainer, **kwargs: Any) -> object: + def __call__(self, widget: ScrollContainer, /, **kwargs: Any) -> object: """A handler to invoke when the container is scrolled. :param widget: The ScrollContainer that was scrolled. diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 2727b9bd41..9b9fb5d565 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -14,7 +14,7 @@ class OnChangeHandler(Protocol): - def __call__(self, widget: Selection, **kwargs: Any) -> object: + def __call__(self, widget: Selection, /, **kwargs: Any) -> object: """A handler to invoke when the value is changed. :param widget: The Selection that was changed. diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 5d91513dbe..efddce2312 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -12,7 +12,7 @@ class OnChangeHandler(Protocol): - def __call__(self, widget: Slider, **kwargs: Any) -> object: + def __call__(self, widget: Slider, /, **kwargs: Any) -> object: """A handler to invoke when the value is changed. :param widget: The Slider that was changed. @@ -21,7 +21,7 @@ def __call__(self, widget: Slider, **kwargs: Any) -> object: class OnPressHandler(Protocol): - def __call__(self, widget: Slider, **kwargs) -> object: + def __call__(self, widget: Slider, /, **kwargs: Any) -> object: """A handler to invoke when the slider is pressed. :param widget: The Slider that was pressed. @@ -30,7 +30,7 @@ def __call__(self, widget: Slider, **kwargs) -> object: class OnReleaseHandler(Protocol): - def __call__(self, widget: Slider, **kwargs: Any) -> object: + def __call__(self, widget: Slider, /, **kwargs: Any) -> object: """A handler to invoke when the slider is pressed. :param widget: The Slider that was released. diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 0ec3a0b95e..17c0dfb354 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -9,7 +9,7 @@ class OnChangeHandler(Protocol): - def __call__(self, widget: Switch, **kwargs: Any) -> object: + def __call__(self, widget: Switch, /, **kwargs: Any) -> object: """A handler to invoke when the value is changed. :param widget: The Switch that was changed. diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index b35f6b3d19..d0e991e0fb 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -15,7 +15,7 @@ class OnSelectHandler(Protocol): - def __call__(self, widget: Table, **kwargs: Any) -> object: + def __call__(self, widget: Table, /, **kwargs: Any) -> object: """A handler to invoke when the table is selected. :param widget: The Table that was selected. @@ -24,7 +24,7 @@ def __call__(self, widget: Table, **kwargs: Any) -> object: class OnActivateHandler(Protocol): - def __call__(self, widget: Table, row: Any, **kwargs: Any) -> object: + def __call__(self, widget: Table, /, row: Any, **kwargs: Any) -> object: """A handler to invoke when the table is activated. :param widget: The Table that was activated. diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index 0302970b9d..cea1d1e44a 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -10,7 +10,7 @@ class OnChangeHandler(Protocol): - def __call__(self, widget: TextInput, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, /, **kwargs: Any) -> object: """A handler to invoke when the text input is changed. :param widget: The TextInput that was changed. @@ -19,7 +19,7 @@ def __call__(self, widget: TextInput, **kwargs: Any) -> object: class OnConfirmHandler(Protocol): - def __call__(self, widget: TextInput, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, /, **kwargs: Any) -> object: """A handler to invoke when the text input is confirmed. :param widget: The TextInput that was confirmed. @@ -28,7 +28,7 @@ def __call__(self, widget: TextInput, **kwargs: Any) -> object: class OnGainFocusHandler(Protocol): - def __call__(self, widget: TextInput, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, /, **kwargs: Any) -> object: """A handler to invoke when the text input gains focus. :param widget: The TextInput that gained focus. @@ -37,7 +37,7 @@ def __call__(self, widget: TextInput, **kwargs: Any) -> object: class OnLoseFocusHandler(Protocol): - def __call__(self, widget: TextInput, **kwargs: Any) -> object: + def __call__(self, widget: TextInput, /, **kwargs: Any) -> object: """A handler to invoke when the text input loses focus. :param widget: The TextInput that lost focus. diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 530747e27e..0a23987bb0 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -11,7 +11,7 @@ class OnChangeHandler(Protocol): - def __call__(self, widget: TimeInput, **kwargs: Any) -> object: + def __call__(self, widget: TimeInput, /, **kwargs: Any) -> object: """A handler to invoke when the time input is changed. :param widget: The TimeInput that was changed. diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 5809f359e5..4c2b2c616e 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -16,7 +16,7 @@ class OnSelectHandler(Protocol): - def __call__(self, widget: Tree, **kwargs: Any) -> object: + def __call__(self, widget: Tree, /, **kwargs: Any) -> object: """A handler to invoke when the tree is selected. :param widget: The Tree that was selected. @@ -25,7 +25,7 @@ def __call__(self, widget: Tree, **kwargs: Any) -> object: class OnActivateHandler(Protocol): - def __call__(self, widget: Tree, **kwargs: Any) -> object: + def __call__(self, widget: Tree, /, **kwargs: Any) -> object: """A handler to invoke when the tree is activated. :param widget: The Tree that was activated. diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 5b15857206..740d830af2 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -1,10 +1,9 @@ from __future__ import annotations import asyncio -from collections.abc import Callable from typing import Any, Protocol -from toga.handlers import AsyncResult, wrapped_handler +from toga.handlers import AsyncResult, OnResultT, wrapped_handler from .base import StyleT, Widget @@ -14,7 +13,7 @@ class JavaScriptResult(AsyncResult): class OnWebViewLoadHandler(Protocol): - def __call__(self, widget: WebView, **kwargs: Any) -> object: + def __call__(self, widget: WebView, /, **kwargs: Any) -> object: """A handler to invoke when the WebView is loaded. :param widget: The WebView that was loaded. @@ -137,7 +136,7 @@ def set_content(self, root_url: str, content: str) -> None: def evaluate_javascript( self, javascript: str, - on_result: Callable[[], object] | None = None, + on_result: OnResultT | None = None, ) -> JavaScriptResult: """Evaluate a JavaScript expression. diff --git a/core/src/toga/window.py b/core/src/toga/window.py index bdc47f5490..c54d56bb46 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -71,7 +71,7 @@ def values(self) -> Iterator[Widget]: class OnCloseHandler(Protocol): - def __call__(self, window: Window, **kwargs: Any) -> bool: + def __call__(self, window: Window, /, **kwargs: Any) -> bool: """A handler to invoke when a window is about to close. The return value of this callback controls whether the window is allowed to close. @@ -84,11 +84,13 @@ def __call__(self, window: Window, **kwargs: Any) -> bool: """ -_DialogResultT = TypeVar("_DialogResultT", contravariant=True) +_DialogResultT = TypeVar("_DialogResultT") class DialogResultHandler(Protocol[_DialogResultT]): - def __call__(self, window: Window, result: _DialogResultT, **kwargs: Any) -> object: + def __call__( + self, window: Window, result: _DialogResultT, /, **kwargs: Any + ) -> object: """A handler to invoke when a dialog is closed. :param window: The window that opened the dialog. From 2f329901a28e5077fcd0172ba317e2e670a58fd0 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Fri, 7 Jun 2024 20:24:05 -0400 Subject: [PATCH 12/13] Remove leftover TODOs --- core/src/toga/sources/list_source.py | 1 - core/src/toga/widgets/detailedlist.py | 1 - core/src/toga/widgets/mapview.py | 1 - core/src/toga/window.py | 1 - 4 files changed, 4 deletions(-) diff --git a/core/src/toga/sources/list_source.py b/core/src/toga/sources/list_source.py index f64aeaa7d7..666bc71925 100644 --- a/core/src/toga/sources/list_source.py +++ b/core/src/toga/sources/list_source.py @@ -96,7 +96,6 @@ def __delattr__(self, attr: str) -> None: self._source.notify("change", item=self) -# TODO:PR: consider adding supported Protocols...maybe List? class ListSource(Source): _data: list[Row] diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index 676f0e1361..a5ec7937e0 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -110,7 +110,6 @@ def __init__( self._secondary_action = secondary_action self.on_select = None - # TODO:PR: in reality, _data needs to be Sized and SupportsIndex... self._data: SourceT | ListSource = None self._impl = self.factory.DetailedList(interface=self) diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index 0ba7b8ab40..dd69651b07 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -75,7 +75,6 @@ def subtitle(self, subtitle: str | None) -> None: self.interface._impl.update_pin(self) -# TODO:PR: __contains__() is required to subclass Set; should it not be added? class MapPinSet: def __init__(self, interface: MapView, pins: Iterable[MapPin] | None): self.interface = interface diff --git a/core/src/toga/window.py b/core/src/toga/window.py index f4fafb5cea..725e5c4a9e 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -105,7 +105,6 @@ class Dialog(AsyncResult): RESULT_TYPE = "dialog" def __init__(self, window: Window, on_result: DialogResultHandler[Any]): - # TODO:PR: should DialogResultHandlerT include the "exception" arg... super().__init__(on_result=on_result) self.window = window self.app = window.app From 9145b98204c06dbe5885c3da28d94a856e1fc056 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Mon, 10 Jun 2024 14:50:48 -0400 Subject: [PATCH 13/13] Update `TypeAlias` naming and documentation --- core/src/toga/app.py | 12 +++---- core/src/toga/command.py | 10 +++--- core/src/toga/icons.py | 2 +- core/src/toga/images.py | 17 +++++----- core/src/toga/plugins/image_formats.py | 4 +-- core/src/toga/widgets/button.py | 10 +++--- core/src/toga/widgets/imageview.py | 10 +++--- core/src/toga/widgets/numberinput.py | 9 +++--- core/src/toga/widgets/optioncontainer.py | 32 +++++++++---------- core/src/toga/widgets/splitcontainer.py | 21 +++++++----- .../api/containers/optioncontainer.rst | 6 ++-- .../api/containers/splitcontainer.rst | 13 ++++++-- docs/reference/api/resources/icons.rst | 2 +- docs/reference/api/resources/images.rst | 6 ++-- docs/reference/api/widgets/numberinput.rst | 6 ++-- dummy/src/toga_dummy/plugins/image_formats.py | 6 ++-- 16 files changed, 89 insertions(+), 77 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 6980ac577b..79e972ca44 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -27,7 +27,7 @@ from toga.window import OnCloseHandler, Window if TYPE_CHECKING: - from toga.icons import IconContent + from toga.icons import IconContentT from toga.types import PositionT, SizeT # Make sure deprecation warnings are shown by default @@ -314,7 +314,7 @@ def __init__( app_id: str | None = None, app_name: str | None = None, *, - icon: IconContent | None = None, + icon: IconContentT | None = None, author: str | None = None, version: str | None = None, home_page: str | None = None, @@ -345,7 +345,7 @@ def __init__( For example, an ``app_id`` of ``com.example.my-app`` would yield a distribution name of ``my-app``. #. As a last resort, the name ``toga``. - :param icon: The :any:`icon ` for the app. Defaults to + :param icon: The :any:`icon ` for the app. Defaults to :attr:`toga.Icon.APP_ICON`. :param author: The person or organization to be credited as the author of the app. If not provided, the metadata key ``Author`` will be used. @@ -535,12 +535,12 @@ def home_page(self) -> str | None: def icon(self) -> Icon: """The Icon for the app. - Can be specified as any valid :any:`icon content `. + Can be specified as any valid :any:`icon content `. """ return self._icon @icon.setter - def icon(self, icon_or_name: IconContent) -> None: + def icon(self, icon_or_name: IconContentT) -> None: if isinstance(icon_or_name, Icon): self._icon = icon_or_name else: @@ -855,7 +855,7 @@ def __init__( app_id: str | None = None, app_name: str | None = None, *, - icon: IconContent | None = None, + icon: IconContentT | None = None, author: str | None = None, version: str | None = None, home_page: str | None = None, diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 167bde400d..ba6bb5f956 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from toga.app import App - from toga.icons import IconContent + from toga.icons import IconContentT class Group: @@ -167,7 +167,7 @@ def __init__( *, shortcut: str | Key | None = None, tooltip: str | None = None, - icon: IconContent | None = None, + icon: IconContentT | None = None, group: Group = Group.COMMANDS, section: int = 0, order: int = 0, @@ -184,7 +184,7 @@ def __init__( :param text: A label for the command. :param shortcut: A key combination that can be used to invoke the command. :param tooltip: A short description of what the command will do. - :param icon: The :any:`icon content ` that can be used to decorate + :param icon: The :any:`icon content ` that can be used to decorate the command if the platform requires. :param group: The group to which this command belongs. :param section: The section where the command should appear within its group. @@ -234,12 +234,12 @@ def enabled(self, value: bool) -> None: def icon(self) -> Icon | None: """The Icon for the command. - Can be specified as any valid :any:`icon content `. + Can be specified as any valid :any:`icon content `. """ return self._icon @icon.setter - def icon(self, icon_or_name: IconContent | None) -> None: + def icon(self, icon_or_name: IconContentT | None) -> None: if isinstance(icon_or_name, Icon) or icon_or_name is None: self._icon = icon_or_name else: diff --git a/core/src/toga/icons.py b/core/src/toga/icons.py index 810322bc54..8d45474eb7 100644 --- a/core/src/toga/icons.py +++ b/core/src/toga/icons.py @@ -15,7 +15,7 @@ else: from typing import TypeAlias - IconContent: TypeAlias = str | Path | toga.Icon + IconContentT: TypeAlias = str | Path | toga.Icon class cachedicon: diff --git a/core/src/toga/images.py b/core/src/toga/images.py index c2c3fb4f97..eb4d9fc668 100644 --- a/core/src/toga/images.py +++ b/core/src/toga/images.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib +import os import sys import warnings from functools import lru_cache @@ -31,10 +32,10 @@ ImageT = TypeVar("ImageT") # Define the types that can be used as Image content - PathLike: TypeAlias = str | Path - BytesLike: TypeAlias = bytes | bytearray | memoryview - ImageLike: TypeAlias = Any - ImageContent: TypeAlias = PathLike | BytesLike | ImageLike + PathLikeT: TypeAlias = str | os.PathLike + BytesLikeT: TypeAlias = bytes | bytearray | memoryview + ImageLikeT: TypeAlias = Any + ImageContentT: TypeAlias = PathLikeT | BytesLikeT | ImageLikeT # Define a type variable representing an image of an externally defined type. ExternalImageT = TypeVar("ExternalImageT") @@ -49,7 +50,7 @@ class ImageConverter(Protocol): image_class: type[ExternalImageT] @staticmethod - def convert_from_format(image_in_format: ExternalImageT) -> BytesLike: + def convert_from_format(image_in_format: ExternalImageT) -> BytesLikeT: """Convert from :any:`image_class` to data in a :ref:`known image format `. @@ -61,7 +62,7 @@ def convert_from_format(image_in_format: ExternalImageT) -> BytesLike: @staticmethod def convert_to_format( - data: BytesLike, + data: BytesLikeT, image_class: type[ExternalImageT], ) -> ExternalImageT: """Convert from data to :any:`image_class` or specified subclass. @@ -83,7 +84,7 @@ def convert_to_format( class Image: def __init__( self, - src: ImageContent = NOT_PROVIDED, + src: ImageContentT = NOT_PROVIDED, *, path: object = NOT_PROVIDED, # DEPRECATED data: object = NOT_PROVIDED, # DEPRECATED @@ -91,7 +92,7 @@ def __init__( """Create a new image. :param src: The source from which to load the image. Can be any valid - :any:`image content ` type. + :any:`image content ` type. :param path: **DEPRECATED** - Use ``src``. :param data: **DEPRECATED** - Use ``src``. :raises FileNotFoundError: If a path is provided, but that path does not exist. diff --git a/core/src/toga/plugins/image_formats.py b/core/src/toga/plugins/image_formats.py index 4bd5778397..4787a3267b 100644 --- a/core/src/toga/plugins/image_formats.py +++ b/core/src/toga/plugins/image_formats.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from toga.images import BytesLike + from toga.images import BytesLikeT # Presumably, other converter plugins will be included with, or only installed # alongside, the packages they're for. But since this is provided in Toga, we need to @@ -29,7 +29,7 @@ def convert_from_format(image_in_format: PIL.Image.Image) -> bytes: @staticmethod def convert_to_format( - data: BytesLike, + data: BytesLikeT, image_class: type[PIL.Image.Image], ) -> PIL.Image.Image: # PIL Images aren't designed to be subclassed, so no implementation is necessary diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index 6f34389d8a..c6ab49b7b1 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -8,7 +8,7 @@ from .base import StyleT, Widget if TYPE_CHECKING: - from toga.icons import IconContent + from toga.icons import IconContentT class OnPressHandler(Protocol): @@ -24,7 +24,7 @@ class Button(Widget): def __init__( self, text: str | None = None, - icon: IconContent | None = None, + icon: IconContentT | None = None, id: str | None = None, style: StyleT | None = None, on_press: toga.widgets.button.OnPressHandler | None = None, @@ -34,7 +34,7 @@ def __init__( :param text: The text to display on the button. :param icon: The icon to display on the button. Can be specified as any valid - :any:`icon content `. + :any:`icon content `. :param id: The ID for the widget. :param style: A style object. If no style is provided, a default style will be applied to the widget. @@ -100,7 +100,7 @@ def text(self, value: str | None) -> None: def icon(self) -> toga.Icon | None: """The icon displayed on the button. - Can be specified as any valid :any:`icon content `. + Can be specified as any valid :any:`icon content `. If the button is currently displaying text, and an icon is assigned, the text will be replaced by the new icon. @@ -113,7 +113,7 @@ def icon(self) -> toga.Icon | None: return self._impl.get_icon() @icon.setter - def icon(self, value: IconContent | None) -> None: + def icon(self, value: IconContentT | None) -> None: if isinstance(value, toga.Icon): icon = value text = "" diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 6826a3c32d..663b3d1071 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -9,7 +9,7 @@ from toga.widgets.base import StyleT, Widget if TYPE_CHECKING: - from toga.images import ImageContent, ImageT + from toga.images import ImageContentT, ImageT def rehint_imageview( @@ -70,7 +70,7 @@ def rehint_imageview( class ImageView(Widget): def __init__( self, - image: ImageContent | None = None, + image: ImageContentT | None = None, id: str | None = None, style: StyleT | None = None, ): @@ -78,7 +78,7 @@ def __init__( Create a new image view. :param image: The image to display. Can be any valid :any:`image content - ` type; or :any:`None` to display no image. + ` type; or :any:`None` to display no image. :param id: The ID for the widget. :param style: A style object. If no style is provided, a default style will be applied to the widget. @@ -111,12 +111,12 @@ def image(self) -> toga.Image | None: """The image to display. When setting an image, you can provide any valid :any:`image content - ` type; or :any:`None` to clear the image view. + ` type; or :any:`None` to clear the image view. """ return self._image @image.setter - def image(self, image: ImageContent) -> None: + def image(self, image: ImageContentT) -> None: if isinstance(image, toga.Image): self._image = image elif image is None: diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index b50f556219..8a9907fe67 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -17,8 +17,7 @@ else: from typing import TypeAlias - NumberInputT: TypeAlias = Union[Decimal, float, str] - StepInputT: TypeAlias = Union[Decimal, int] + NumberInputT: TypeAlias = Union[Decimal, int, float, str] # Implementation notes # ==================== @@ -35,7 +34,7 @@ NUMERIC_RE = re.compile(r"[^0-9\.-]") -def _clean_decimal(value: NumberInputT, step: StepInputT | None = None) -> Decimal: +def _clean_decimal(value: NumberInputT, step: NumberInputT | None = None) -> Decimal: # Decimal(3.7) yields "3.700000000...177". # However, Decimal(str(3.7)) yields "3.7". If the user provides a float, # convert to a string first. @@ -87,7 +86,7 @@ def __init__( self, id: str | None = None, style: StyleT | None = None, - step: StepInputT = 1, + step: NumberInputT = 1, min: NumberInputT | None = None, max: NumberInputT | None = None, value: NumberInputT | None = None, @@ -181,7 +180,7 @@ def step(self) -> Decimal: return self._step @step.setter - def step(self, step: StepInputT) -> None: + def step(self, step: NumberInputT) -> None: try: self._step = _clean_decimal(step) except (ValueError, TypeError, InvalidOperation): diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index 5de0337fd1..8716ebb7c6 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -15,12 +15,12 @@ from typing_extensions import TypeAlias else: from typing import TypeAlias - from toga.icons import IconContent + from toga.icons import IconContentT - OptionContainerContent: TypeAlias = ( + OptionContainerContentT: TypeAlias = ( tuple[str, Widget] - | tuple[str, Widget, IconContent | None] - | tuple[str, Widget, IconContent | None, bool] + | tuple[str, Widget, IconContentT | None] + | tuple[str, Widget, IconContentT | None, bool] | toga.OptionItem ) @@ -40,14 +40,14 @@ def __init__( text: str, content: Widget, *, - icon: IconContent | None = None, + icon: IconContentT | None = None, enabled: bool = True, ): """A tab of content in an OptionContainer. :param text: The text label for the new tab. :param content: The content widget to use for the new tab. - :param icon: The :any:`icon content ` to use to represent the tab. + :param icon: The :any:`icon content ` to use to represent the tab. :param enabled: Should the new tab be enabled? """ if content is None: @@ -128,7 +128,7 @@ def text(self, value: object) -> None: def icon(self) -> toga.Icon: """The Icon for the tab of content. - Can be specified as any valid :any:`icon content `. + Can be specified as any valid :any:`icon content `. If the platform does not support the display of icons, this property will return ``None`` regardless of any value provided. @@ -139,7 +139,7 @@ def icon(self) -> toga.Icon: return self._interface._impl.get_option_icon(self.index) @icon.setter - def icon(self, icon_or_name: IconContent | None) -> None: + def icon(self, icon_or_name: IconContentT | None) -> None: if get_platform_factory().OptionContainer.uses_icons: if icon_or_name is None: icon = None @@ -274,7 +274,7 @@ def append( text_or_item: str, content: Widget, *, - icon: IconContent | None = None, + icon: IconContentT | None = None, enabled: bool | None = True, ) -> None: ... @@ -283,7 +283,7 @@ def append( text_or_item: str | OptionItem, content: Widget | None = None, *, - icon: IconContent | None = None, + icon: IconContentT | None = None, enabled: bool | None = None, ) -> None: """Add a new tab of content to the OptionContainer. @@ -295,7 +295,7 @@ def append( :param text_or_item: An :any:`OptionItem`; or, the text label for the new tab. :param content: The content widget to use for the new tab. - :param icon: The :any:`icon content ` to use to represent the tab. + :param icon: The :any:`icon content ` to use to represent the tab. :param enabled: Should the new tab be enabled? (Default: ``True``) """ self.insert(len(self), text_or_item, content, icon=icon, enabled=enabled) @@ -314,7 +314,7 @@ def insert( text_or_item: str, content: Widget, *, - icon: IconContent | None = None, + icon: IconContentT | None = None, enabled: bool | None = True, ) -> None: ... @@ -324,7 +324,7 @@ def insert( text_or_item: str | OptionItem, content: Widget | None = None, *, - icon: IconContent | None = None, + icon: IconContentT | None = None, enabled: bool | None = None, ) -> None: """Insert a new tab of content to the OptionContainer at the specified index. @@ -337,7 +337,7 @@ def insert( :param index: The index where the new tab should be inserted. :param text_or_item: An :any:`OptionItem`; or, the text label for the new tab. :param content: The content widget to use for the new tab. - :param icon: The :any:`icon content ` to use to represent the tab. + :param icon: The :any:`icon content ` to use to represent the tab. :param enabled: Should the new tab be enabled? (Default: ``True``) """ if isinstance(text_or_item, OptionItem): @@ -380,7 +380,7 @@ def __init__( self, id: str | None = None, style: StyleT | None = None, - content: Iterable[OptionContainerContent] | None = None, + content: Iterable[OptionContainerContentT] | None = None, on_select: toga.widgets.optioncontainer.OnSelectHandler | None = None, ): """Create a new OptionContainer. @@ -389,7 +389,7 @@ def __init__( :param style: A style object. If no style is provided, a default style will be applied to the widget. :param content: The initial :any:`OptionContainer content - ` to display in the OptionContainer. + ` to display in the OptionContainer. :param on_select: Initial :any:`on_select` handler. """ super().__init__(id=id, style=style) diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 7dc120d667..04589bc737 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING, Tuple, Union +from typing import TYPE_CHECKING from toga.app import App from toga.constants import Direction @@ -15,7 +15,7 @@ else: from typing import TypeAlias - ContentT: TypeAlias = Union[Widget, Tuple[Widget, float], None] + SplitContainerContentT: TypeAlias = Widget | tuple[Widget, float] | None class SplitContainer(Widget): @@ -27,7 +27,7 @@ def __init__( id: str | None = None, style: StyleT | None = None, direction: Direction = Direction.VERTICAL, - content: tuple[ContentT, ContentT] = (None, None), + content: tuple[SplitContainerContentT, SplitContainerContentT] = (None, None), ): """Create a new SplitContainer. @@ -38,11 +38,14 @@ def __init__( :attr:`~toga.constants.Direction.HORIZONTAL` or :attr:`~toga.constants.Direction.VERTICAL`; defaults to :attr:`~toga.constants.Direction.VERTICAL` - :param content: Initial :any:`content` of the container. Defaults to both panels - being empty. + :param content: Initial :any:`SplitContainer content ` + of the container. Defaults to both panels being empty. """ super().__init__(id=id, style=style) - self._content: tuple[ContentT, ContentT] = (None, None) + self._content: tuple[SplitContainerContentT, SplitContainerContentT] = ( + None, + None, + ) # Create a platform specific implementation of a SplitContainer self._impl = self.factory.SplitContainer(interface=self) @@ -68,7 +71,7 @@ def focus(self) -> None: pass @property - def content(self) -> tuple[ContentT, ContentT]: + def content(self) -> tuple[SplitContainerContentT, SplitContainerContentT]: """The widgets displayed in the SplitContainer. This property accepts a sequence of exactly 2 elements, each of which can be @@ -86,7 +89,9 @@ def content(self) -> tuple[ContentT, ContentT]: return self._content @content.setter - def content(self, content: tuple[ContentT, ContentT]) -> None: + def content( + self, content: tuple[SplitContainerContentT, SplitContainerContentT] + ) -> None: try: if len(content) != 2: raise TypeError() diff --git a/docs/reference/api/containers/optioncontainer.rst b/docs/reference/api/containers/optioncontainer.rst index 728a53d51e..b6e3d79a0e 100644 --- a/docs/reference/api/containers/optioncontainer.rst +++ b/docs/reference/api/containers/optioncontainer.rst @@ -174,14 +174,14 @@ Notes Reference --------- -.. c:type:: OptionContainerContent +.. c:type:: OptionContainerContentT An item of :any:`OptionContainer` content can be: * a 2-tuple, containing the title for the tab, and the content widget; - * a 3-tuple, containing the title, content widget, and :any:`icon ` + * a 3-tuple, containing the title, content widget, and :any:`icon ` for the tab; - * a 4-tuple, containing the title, content widget, :any:`icon ` for + * a 4-tuple, containing the title, content widget, :any:`icon ` for the tab, and enabled status; or * an :any:`toga.OptionItem` instance. diff --git a/docs/reference/api/containers/splitcontainer.rst b/docs/reference/api/containers/splitcontainer.rst index 48c0ea8e13..6d04683daf 100644 --- a/docs/reference/api/containers/splitcontainer.rst +++ b/docs/reference/api/containers/splitcontainer.rst @@ -49,7 +49,7 @@ Usage left_container = toga.Box() right_container = toga.ScrollContainer() - split = toga.SplitContainer(content=[left_container, right_container]) + split = toga.SplitContainer(content=(left_container, right_container)) Content can be specified when creating the widget, or after creation by assigning the ``content`` attribute. The direction of the split can also be configured, either @@ -65,7 +65,7 @@ at time of creation, or by setting the ``direction`` attribute: left_container = toga.Box() right_container = toga.ScrollContainer() - split.content = [left_container, right_container] + split.content = (left_container, right_container) By default, the space of the SplitContainer will be evenly divided between the two panels. To specify an uneven split, you can provide a flex value when specifying @@ -80,7 +80,7 @@ and right panels. left_container = toga.Box() right_container = toga.ScrollContainer() - split.content = [(left_container, 3), (right_container, 2)] + split.content = ((left_container, 3), (right_container, 2)) This only specifies the initial split; the split can be modified by the user once it is displayed. @@ -88,5 +88,12 @@ once it is displayed. Reference --------- +.. c:type:: SplitContainerContentT + + An item of :any:`SplitContainer` content can be: + + * a :class:`~toga.Widget`; or + * a 2-tuple, containing a :class:`~toga.Widget`, and an :any:`int` flex value + .. autoclass:: toga.SplitContainer :exclude-members: HORIZONTAL, VERTICAL, window, app diff --git a/docs/reference/api/resources/icons.rst b/docs/reference/api/resources/icons.rst index 90cdfad228..3f517d4ef7 100644 --- a/docs/reference/api/resources/icons.rst +++ b/docs/reference/api/resources/icons.rst @@ -80,7 +80,7 @@ permission error). In this case, an error will be raised. Reference --------- -.. c:type:: IconContent +.. c:type:: IconContentT When specifying an :any:`Icon`, you can provide: diff --git a/docs/reference/api/resources/images.rst b/docs/reference/api/resources/images.rst index 59aa204451..498d2ccee8 100644 --- a/docs/reference/api/resources/images.rst +++ b/docs/reference/api/resources/images.rst @@ -30,7 +30,7 @@ Usage widget configured to use an Icon (probably :class:`~toga.Button`), *not* an ``on_press`` handler on an :class:`~toga.Image` or :class:`~toga.ImageView`. -An image can be constructed from a :any:`wide range of sources `: +An image can be constructed from a :any:`wide range of sources `: .. code-block:: python @@ -86,13 +86,13 @@ Notes Reference --------- -.. c:type:: ImageContent +.. c:type:: ImageContentT When specifying content for an :any:`Image`, you can provide: * a string specifying an absolute or relative path to a file in a :ref:`known image format `; - * an absolute or relative :any:`pathlib.Path` object describing a file in a + * an absolute or relative :class:`~pathlib.Path` object describing a file in a :ref:`known image format `; * a "blob of bytes" data type (:any:`bytes`, :any:`bytearray`, or :any:`memoryview`) containing raw image data in a :ref:`known image format `; diff --git a/docs/reference/api/widgets/numberinput.rst b/docs/reference/api/widgets/numberinput.rst index 950aad45b0..77b532a693 100644 --- a/docs/reference/api/widgets/numberinput.rst +++ b/docs/reference/api/widgets/numberinput.rst @@ -53,9 +53,9 @@ Usage widget = toga.NumberInput(min_value=1, max_value=10, step=0.001) widget.value = 2.718 -NumberInput's properties can accept integers, floats, and strings containing -numbers, but they always return :any:`decimal.Decimal` objects to ensure -precision is retained. +NumberInput's properties can accept :class:`~decimal.Decimal`, :any:`int`, :any:`float`, +or :any:`str` containing numbers, but they always return :class:`~decimal.Decimal` +objects to ensure precision is retained. Reference --------- diff --git a/dummy/src/toga_dummy/plugins/image_formats.py b/dummy/src/toga_dummy/plugins/image_formats.py index d58686b739..923ceb418c 100644 --- a/dummy/src/toga_dummy/plugins/image_formats.py +++ b/dummy/src/toga_dummy/plugins/image_formats.py @@ -6,7 +6,7 @@ import toga if TYPE_CHECKING: - from toga.images import BytesLike + from toga.images import BytesLikeT class CustomImage: @@ -26,7 +26,7 @@ def convert_from_format(image_in_format: CustomImage): @staticmethod def convert_to_format( - data: BytesLike, + data: BytesLikeT, image_class: type[CustomImage], ) -> CustomImage: image = image_class() @@ -45,7 +45,7 @@ def convert_from_format(image_in_format: Any): @staticmethod def convert_to_format( - data: BytesLike, + data: BytesLikeT, image_class: type[Any], ) -> Any: raise Exception("Converter should be disabled")