diff --git a/android/src/toga_android/widgets/slider.py b/android/src/toga_android/widgets/slider.py index 12a263b152..d0c5988931 100644 --- a/android/src/toga_android/widgets/slider.py +++ b/android/src/toga_android/widgets/slider.py @@ -5,7 +5,7 @@ from android.widget import SeekBar from java import dynamic_proxy -import toga +from toga.widgets.slider import IntSliderImpl from .base import Widget @@ -31,7 +31,7 @@ def onStopTrackingTouch(self, native_seekbar): self.impl.interface.on_release() -class Slider(Widget, toga.widgets.slider.IntSliderImpl): +class Slider(Widget, IntSliderImpl): focusable = False TICK_DRAWABLE = None diff --git a/changes/2547.misc.rst b/changes/2547.misc.rst new file mode 100644 index 0000000000..488c5290b3 --- /dev/null +++ b/changes/2547.misc.rst @@ -0,0 +1 @@ +Imports from the ``toga`` core namespace has been modified to use lazy importing. diff --git a/cocoa/src/toga_cocoa/widgets/slider.py b/cocoa/src/toga_cocoa/widgets/slider.py index 98d86b9642..3154c674c7 100644 --- a/cocoa/src/toga_cocoa/widgets/slider.py +++ b/cocoa/src/toga_cocoa/widgets/slider.py @@ -1,6 +1,6 @@ from travertino.size import at_least -import toga +from toga.widgets.slider import SliderImpl from toga_cocoa.libs import ( SEL, NSEventType, @@ -27,7 +27,7 @@ def onSlide_(self, sender) -> None: self.interface.on_change() -class Slider(Widget, toga.widgets.slider.SliderImpl): +class Slider(Widget, SliderImpl): def create(self): self.native = TogaSlider.alloc().init() self.native.interface = self.interface diff --git a/core/src/toga/__init__.py b/core/src/toga/__init__.py index 2f7679bd88..f233939c44 100644 --- a/core/src/toga/__init__.py +++ b/core/src/toga/__init__.py @@ -1,55 +1,97 @@ from __future__ import annotations +import importlib import warnings from pathlib import Path -from .app import App, DocumentApp -from .colors import hsl, hsla, rgb, rgba -from .command import Command, Group -from .dialogs import ( - ConfirmDialog, - ErrorDialog, - InfoDialog, - OpenFileDialog, - QuestionDialog, - SaveFileDialog, - SelectFolderDialog, - StackTraceDialog, -) -from .documents import Document, DocumentWindow -from .fonts import Font -from .icons import Icon -from .images import Image -from .keys import Key -from .statusicons import MenuStatusIcon, SimpleStatusIcon -from .types import LatLng, Position, Size -from .widgets.activityindicator import ActivityIndicator -from .widgets.base import Widget -from .widgets.box import Box -from .widgets.button import Button -from .widgets.canvas import Canvas -from .widgets.dateinput import DateInput, DatePicker -from .widgets.detailedlist import DetailedList -from .widgets.divider import Divider -from .widgets.imageview import ImageView -from .widgets.label import Label -from .widgets.mapview import MapPin, MapView -from .widgets.multilinetextinput import MultilineTextInput -from .widgets.numberinput import NumberInput -from .widgets.optioncontainer import OptionContainer, OptionItem -from .widgets.passwordinput import PasswordInput -from .widgets.progressbar import ProgressBar -from .widgets.scrollcontainer import ScrollContainer -from .widgets.selection import Selection -from .widgets.slider import Slider -from .widgets.splitcontainer import SplitContainer -from .widgets.switch import Switch -from .widgets.table import Table -from .widgets.textinput import TextInput -from .widgets.timeinput import TimeInput, TimePicker -from .widgets.tree import Tree -from .widgets.webview import WebView -from .window import MainWindow, Window +toga_core_imports = { + # toga.app imports + "App": "toga.app", + "DocumentApp": "toga.app", + # toga.colors imports + "hsl": "toga.colors", + "hsla": "toga.colors", + "rgb": "toga.colors", + "rgba": "toga.colors", + # toga.command imports + "Command": "toga.command", + "Group": "toga.command", + # toga.dialogs imports + "ConfirmDialog": "toga.dialogs", + "ErrorDialog": "toga.dialogs", + "InfoDialog": "toga.dialogs", + "OpenFileDialog": "toga.dialogs", + "QuestionDialog": "toga.dialogs", + "SaveFileDialog": "toga.dialogs", + "SelectFolderDialog": "toga.dialogs", + "StackTraceDialog": "toga.dialogs", + # toga.documents imports + "Document": "toga.documents", + "DocumentWindow": "toga.documents", + # toga.fonts imports + "Font": "toga.fonts", + # toga.icons imports + "Icon": "toga.icons", + # toga.images imports + "Image": "toga.images", + # toga.keys imports + "Key": "toga.keys", + # toga.statusicons imports + "MenuStatusIcon": "toga.statusicons", + "SimpleStatusIcon": "toga.statusicons", + # toga.types imports + "LatLng": "toga.types", + "Position": "toga.types", + "Size": "toga.types", + # toga.widgets imports + "ActivityIndicator": "toga.widgets.activityindicator", + "Widget": "toga.widgets.base", + "Box": "toga.widgets.box", + "Button": "toga.widgets.button", + "Canvas": "toga.widgets.canvas", + "DateInput": "toga.widgets.dateinput", + "DatePicker": "toga.widgets.dateinput", + "DetailedList": "toga.widgets.detailedlist", + "Divider": "toga.widgets.divider", + "ImageView": "toga.widgets.imageview", + "Label": "toga.widgets.label", + "MapPin": "toga.widgets.mapview", + "MapView": "toga.widgets.mapview", + "MultilineTextInput": "toga.widgets.multilinetextinput", + "NumberInput": "toga.widgets.numberinput", + "OptionContainer": "toga.widgets.optioncontainer", + "OptionItem": "toga.widgets.optioncontainer", + "PasswordInput": "toga.widgets.passwordinput", + "ProgressBar": "toga.widgets.progressbar", + "ScrollContainer": "toga.widgets.scrollcontainer", + "Selection": "toga.widgets.selection", + "Slider": "toga.widgets.slider", + "SplitContainer": "toga.widgets.splitcontainer", + "Switch": "toga.widgets.switch", + "Table": "toga.widgets.table", + "TextInput": "toga.widgets.textinput", + "TimeInput": "toga.widgets.timeinput", + "TimePicker": "toga.widgets.timeinput", + "Tree": "toga.widgets.tree", + "WebView": "toga.widgets.webview", + # toga.window imports + "DocumentMainWindow": "toga.window", + "MainWindow": "toga.window", + "Window": "toga.window", +} +__all__ = list(toga_core_imports.keys()) + + +def __getattr__(name): + try: + module_name = toga_core_imports[name] + except KeyError: + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") from None + else: + module = importlib.import_module(module_name) + value = getattr(module, name) + globals()[name] = value + return value class NotImplementedWarning(RuntimeWarning): @@ -62,81 +104,6 @@ def warn(cls, platform: str, feature: str) -> None: warnings.warn(NotImplementedWarning(f"[{platform}] Not implemented: {feature}")) -__all__ = [ - "NotImplementedWarning", - # Applications - "App", - "DocumentApp", - # Commands - "Command", - "Group", - # Documents - "Document", - "DocumentWindow", - # Dialogs - "ConfirmDialog", - "ErrorDialog", - "InfoDialog", - "OpenFileDialog", - "QuestionDialog", - "SaveFileDialog", - "SelectFolderDialog", - "StackTraceDialog", - # Keys - "Key", - # Resources - "hsl", - "hsla", - "rgb", - "rgba", - "Font", - "Icon", - "Image", - # Status icons - "MenuStatusIcon", - "SimpleStatusIcon", - # Types - "LatLng", - "Position", - "Size", - # Widgets - "ActivityIndicator", - "Box", - "Button", - "Canvas", - "DateInput", - "DetailedList", - "Divider", - "ImageView", - "Label", - "MapPin", - "MapView", - "MultilineTextInput", - "NumberInput", - "OptionContainer", - "OptionItem", - "PasswordInput", - "ProgressBar", - "ScrollContainer", - "Selection", - "Slider", - "SplitContainer", - "Switch", - "Table", - "TextInput", - "TimeInput", - "Tree", - "WebView", - "Widget", - # Windows - "MainWindow", - "Window", - # Deprecated widget names - "DatePicker", - "TimePicker", -] - - def _package_version(file: Path | str | None, name: str) -> str: try: # Read version from SCM metadata diff --git a/core/tests/test_import.py b/core/tests/test_import.py new file mode 100644 index 0000000000..4ba0ed7e00 --- /dev/null +++ b/core/tests/test_import.py @@ -0,0 +1,38 @@ +import sys + +import pytest + + +def test_lazy_succeed(monkeypatch): + """Submodules are imported on demand.""" + for mod_name in ["toga", "toga.documents", "toga.widgets.button"]: + monkeypatch.delitem(sys.modules, mod_name, raising=False) + + # A clean import of the top-level toga module should not import any submodules. + import toga + + assert "toga.documents" not in sys.modules + assert "toga.widgets.button" not in sys.modules + + # Accessing a name should import only the necessary submodules. + Button = toga.Button + assert "toga.widgets.button" in sys.modules + assert "toga.documents" not in sys.modules + + # Accessing a name multiple times should return the same object. + assert Button is toga.Button + assert Button is sys.modules["toga.widgets.button"].Button + + # Same again with a different module. + Document = toga.Document + assert Document is sys.modules["toga.documents"].Document + + +def test_lazy_fail(): + """Nonexistent names should raise a normal AttributeError.""" + import toga + + with pytest.raises( + AttributeError, match="module 'toga' has no attribute 'nonexistent'" + ): + toga.nonexistent diff --git a/dummy/src/toga_dummy/widgets/slider.py b/dummy/src/toga_dummy/widgets/slider.py index 31c2a4137a..e3ea59a3e0 100644 --- a/dummy/src/toga_dummy/widgets/slider.py +++ b/dummy/src/toga_dummy/widgets/slider.py @@ -1,9 +1,9 @@ -import toga +from toga.widgets.slider import SliderImpl from .base import Widget -class Slider(Widget, toga.widgets.slider.SliderImpl): +class Slider(Widget, SliderImpl): def create(self): self._action("create Slider") diff --git a/gtk/src/toga_gtk/widgets/slider.py b/gtk/src/toga_gtk/widgets/slider.py index 301cbfa356..f5d4766372 100644 --- a/gtk/src/toga_gtk/widgets/slider.py +++ b/gtk/src/toga_gtk/widgets/slider.py @@ -1,6 +1,6 @@ from travertino.size import at_least -import toga +from toga.widgets.slider import SliderImpl from ..libs import Gtk from .base import Widget @@ -16,7 +16,7 @@ # to line up at the same values. -class Slider(Widget, toga.widgets.slider.SliderImpl): +class Slider(Widget, SliderImpl): def create(self): self.adj = Gtk.Adjustment() self.native = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, self.adj)