diff --git a/changes/2547.misc.rst b/changes/2547.misc.rst new file mode 100644 index 0000000000..9d84251bf4 --- /dev/null +++ b/changes/2547.misc.rst @@ -0,0 +1 @@ +Imports from the ``cocoa`` core namespace has been modified to use lazy importing. diff --git a/cocoa/src/toga_cocoa/__init__.py b/cocoa/src/toga_cocoa/__init__.py index 1fb1a84eae..c20224ea13 100644 --- a/cocoa/src/toga_cocoa/__init__.py +++ b/cocoa/src/toga_cocoa/__init__.py @@ -1,3 +1,35 @@ -import travertino +import importlib +from pathlib import Path -__version__ = travertino._package_version(__file__, __name__) +from travertino import _package_version + + +def lazy_load(): + toga_cocoa_imports = {} + pyi = Path(__file__).with_suffix(".pyi") + with pyi.open() as f: + for line in f: + segments = line.split() + if segments and segments[0] == "from": + toga_cocoa_imports[segments[3]] = segments[1] + return toga_cocoa_imports + + +toga_cocoa_imports = lazy_load() + +__all__ = list(toga_cocoa_imports.keys()) + + +def __getattr__(name): + try: + module_name = toga_cocoa_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 + + +__version__ = _package_version(__file__, __name__) diff --git a/cocoa/src/toga_cocoa/__init__.pyi b/cocoa/src/toga_cocoa/__init__.pyi new file mode 100644 index 0000000000..984e814a0e --- /dev/null +++ b/cocoa/src/toga_cocoa/__init__.pyi @@ -0,0 +1,45 @@ +# flake8: noqa +""" +isort:skip_file +""" + +from toga_cocoa.factory import not_implemented as not_implemented +from toga_cocoa.factory import App as App +from toga_cocoa.factory import Command as Command +from toga_cocoa.factory import Font as Font +from toga_cocoa.factory import Icon as Icon +from toga_cocoa.factory import Image as Image +from toga_cocoa.factory import Paths as Paths +from toga_cocoa.factory import dialogs as dialogs +from toga_cocoa.factory import Camera as Camera +from toga_cocoa.factory import Location as Location +from toga_cocoa.factory import MenuStatusIcon as MenuStatusIcon +from toga_cocoa.factory import SimpleStatusIcon as SimpleStatusIcon +from toga_cocoa.factory import StatusIconSet as StatusIconSet +from toga_cocoa.factory import ActivityIndicator as ActivityIndicator +from toga_cocoa.factory import Box as Box +from toga_cocoa.factory import Button as Button +from toga_cocoa.factory import Canvas as Canvas +from toga_cocoa.factory import DateInput as DateInput +from toga_cocoa.factory import DetailedList as DetailedList +from toga_cocoa.factory import Divider as Divider +from toga_cocoa.factory import ImageView as ImageView +from toga_cocoa.factory import Label as Label +from toga_cocoa.factory import MapView as MapView +from toga_cocoa.factory import MultilineTextInput as MultilineTextInput +from toga_cocoa.factory import NumberInput as NumberInput +from toga_cocoa.factory import OptionContainer as OptionContainer +from toga_cocoa.factory import PasswordInput as PasswordInput +from toga_cocoa.factory import ProgressBar as ProgressBar +from toga_cocoa.factory import ScrollContainer as ScrollContainer +from toga_cocoa.factory import Selection as Selection +from toga_cocoa.factory import Slider as Slider +from toga_cocoa.factory import SplitContainer as SplitContainer +from toga_cocoa.factory import Switch as Switch +from toga_cocoa.factory import Table as Table +from toga_cocoa.factory import TextInput as TextInput +from toga_cocoa.factory import TimeInput as TimeInput +from toga_cocoa.factory import Tree as Tree +from toga_cocoa.factory import WebView as WebView +from toga_cocoa.factory import MainWindow as MainWindow +from toga_cocoa.factory import Window as Window diff --git a/cocoa/tests_backend/test_import.py b/cocoa/tests_backend/test_import.py new file mode 100644 index 0000000000..6742bc7f1c --- /dev/null +++ b/cocoa/tests_backend/test_import.py @@ -0,0 +1,61 @@ +import sys + +import pytest + + +def test_lazy_succeed(monkeypatch): + """Submodules are imported on demand.""" + for mod_name in ["toga_cocoa", "toga_cocoa.factory", "toga_cocoa.widgets.button"]: + monkeypatch.delitem(sys.modules, mod_name, raising=False) + + # clean import of the top-level toga_cocoa module should not import any submodules. + import toga_cocoa + + assert "toga_cocoa.factory" not in sys.modules + assert "toga_cocoa.widgets.button" not in sys.modules + + # Accessing a name should import only the necessary submodules. + Button = toga_cocoa.Button + assert "toga_cocoa.factory" in sys.modules + assert "toga_cocoa.widgets.button" in sys.modules + + # Accessing a name multiple times should return the same object. + assert Button is toga_cocoa.Button + assert Button is sys.modules["toga_cocoa.factory"].Button + + # Same again with a different attribute. + App = toga_cocoa.App + assert App is sys.modules["toga_cocoa.factory"].App + + assert hasattr(toga_cocoa, "Button") + assert hasattr(toga_cocoa, "App") + cached_button = toga_cocoa.Button + assert cached_button is Button + + +def test_lazy_fail(): + """Nonexistent names should raise a normal AttributeError.""" + import toga_cocoa + + with pytest.raises( + AttributeError, match="module 'toga_cocoa' has no attribute 'nonexistent'" + ): + _ = toga_cocoa.nonexistent + + +def test_lazy_load_cache(monkeypatch): + import toga_cocoa + + if "Button" in toga_cocoa.__dict__: + del toga_cocoa.__dict__["Button"] + btn = toga_cocoa.Button + assert btn is toga_cocoa.Button + + +def test_lazy_attribute_error(monkeypatch): + import toga_cocoa + + with pytest.raises( + AttributeError, match="module 'toga_cocoa' has no attribute 'nonexistent'" + ): + _ = toga_cocoa.nonexistent