Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2547.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Imports from the ``cocoa`` core namespace has been modified to use lazy importing.
36 changes: 34 additions & 2 deletions cocoa/src/toga_cocoa/__init__.py
Original file line number Diff line number Diff line change
@@ -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__)
45 changes: 45 additions & 0 deletions cocoa/src/toga_cocoa/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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
61 changes: 61 additions & 0 deletions cocoa/tests_backend/test_import.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two cents is that the tests_backend stuff aren't being run as tests -- they just support the test files in the tests directory in the testbed, so this file isn't been ran at all. Not sure if this is right, but I think you'd need to generalize this to not write toga_cocoa but to work out a name to change the tested import depending on backend in the testbed tests directory, and then skip the test on everything besides Cocoa for now.

Original file line number Diff line number Diff line change
@@ -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
Loading