Skip to content

Commit

Permalink
[POC] Typing ergonomics and guarantees
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Dec 3, 2023
1 parent 2db1d59 commit cb569ba
Show file tree
Hide file tree
Showing 19 changed files with 255 additions and 131 deletions.
File renamed without changes.
1 change: 1 addition & 0 deletions changes/2252.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The typing for Toga's API surface is now compliant with mypy.
2 changes: 2 additions & 0 deletions core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ dependencies = [
"travertino >= 0.3.0",
# limited to <=3.9 for the `group` argument for `entry_points()`
"importlib_metadata >= 4.4.0; python_version <= '3.9'",
"typing-extensions == 4.8.0",
]

[project.optional-dependencies]
Expand All @@ -73,6 +74,7 @@ dev = [
"pytest-freezer == 0.4.8",
"setuptools-scm == 8.0.4",
"tox == 4.11.3",
"types-Pillow == 10.1.0.2",
]
docs = [
"furo == 2023.9.10",
Expand Down
89 changes: 56 additions & 33 deletions core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import webbrowser
from collections.abc import Collection, Iterator, Mapping, MutableSet
from email.message import Message
from typing import Any, Protocol
from typing import Any, Protocol, Union
from warnings import warn

from toga.command import Command, CommandSet
from typing_extensions import TypeAlias

from toga.command import CommandSet
from toga.documents import Document
from toga.handlers import wrapped_handler
from toga.handlers import HandlerGeneratorT, wrapped_handler
from toga.icons import Icon
from toga.paths import Paths
from toga.platform import get_platform_factory
Expand All @@ -25,44 +27,65 @@


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.
:returns: :any:`True` if the app is allowed to exit; :any:`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, /) -> HandlerGeneratorT[bool]:
"""Generator definition of :any:`OnExitHandlerSync`."""


OnExitHandlerT: TypeAlias = Union[
OnExitHandlerSync, OnExitHandlerAsync, OnExitHandlerGenerator
]


class BackgroundTaskSync(Protocol):
def __call__(self, app: App, /) -> Any:
"""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 BackgroundTaskAsync(Protocol):
async def __call__(self, app: App, /) -> Any:
"""Async definition of :any:`BackgroundTaskSync`."""


class BackgroundTaskGenerator(Protocol):
def __call__(self, app: App, /) -> HandlerGeneratorT[Any]:
"""Generator definition of :any:`BackgroundTaskSync`."""


BackgroundTaskT: TypeAlias = Union[
BackgroundTaskSync, BackgroundTaskAsync, BackgroundTaskGenerator
]


class WindowSet(MutableSet):
Expand All @@ -74,7 +97,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):
Expand All @@ -95,7 +118,7 @@ def discard(self, window: Window) -> None:
# 2023-10: Backwards compatibility
######################################################################

def __iadd__(self, window: Window) -> None:
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",
Expand All @@ -104,7 +127,7 @@ def __iadd__(self, window: Window) -> None:
)
return self

def __isub__(self, other: Window) -> None:
def __isub__(self, other: Window) -> WindowSet:
# The standard set type does have a -= operator, but it takes sets rather than
# individual items.
warn(
Expand Down Expand Up @@ -238,7 +261,7 @@ def _default_title(self) -> str:


class App:
app = None
app: App

def __init__(
self,
Expand All @@ -252,7 +275,7 @@ def __init__(
home_page: str | None = None,
description: str | None = None,
startup: AppStartupMethod | None = None,
on_exit: OnExitHandler | None = None,
on_exit: OnExitHandlerT | None = None,
id=None, # DEPRECATED
windows=None, # DEPRECATED
):
Expand Down Expand Up @@ -542,12 +565,12 @@ def windows(self, windows):
######################################################################

@property
def commands(self) -> MutableSet[Command]:
def commands(self) -> CommandSet:
"""The commands available in the app."""
return self._commands

@property
def main_window(self) -> MainWindow:
def main_window(self) -> MainWindow | None:
"""The main window for the app."""
return self._main_window

Expand Down Expand Up @@ -671,12 +694,12 @@ def exit(self) -> None:
self._impl.exit()

@property
def on_exit(self) -> OnExitHandler:
def on_exit(self) -> OnExitHandlerT:
"""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 on_exit(self, handler: OnExitHandlerT | None) -> None:
def cleanup(app, should_exit):
if should_exit or handler is None:
app.exit()
Expand All @@ -690,7 +713,7 @@ def loop(self) -> asyncio.AbstractEventLoop:
thread (read-only)."""
return self._impl.loop

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
Expand All @@ -717,15 +740,15 @@ 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,
document_types: dict[str, type[Document]] | None = None,
on_exit: OnExitHandlerT | None = None,
id=None, # DEPRECATED
):
"""Create a document-based application.
A document-based application is the same as a normal application, with the
exception that there is no main window. Instead, each document managed by the
app will create and manage it's own window (or windows).
app will create and manage its own window (or windows).
:param document_types: Initial :any:`document_types` mapping.
"""
Expand Down
1 change: 1 addition & 0 deletions core/src/toga/colors.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# TODO:PR: replace with explicit import - probably will need tests to verify
# Use the Travertino color definitions as-is
from travertino.colors import * # noqa: F401, F403
Loading

0 comments on commit cb569ba

Please sign in to comment.