diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 182447950f..713b1f3220 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -187,7 +187,7 @@ def create(self): # the app's `.native` is the listener's native Java class. self._listener = TogaApp(self) # Call user code to populate the main window - self.interface.startup() + self.interface._startup() def open_document(self, fileURL): print("Can't open document %s (yet)" % fileURL) diff --git a/changes/2047.feature.rst b/changes/2047.feature.rst new file mode 100644 index 0000000000..c5bd725787 --- /dev/null +++ b/changes/2047.feature.rst @@ -0,0 +1 @@ +Applications now verify that a main window has been created as part of the ``startup()`` method. diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index b8be4094f3..c983720d4d 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -255,7 +255,7 @@ def create(self): self._create_app_commands() # Call user code to populate the main window - self.interface.startup() + self.interface._startup() # Create the lookup table of menu items, # then force the creation of the menus. diff --git a/core/src/toga/app.py b/core/src/toga/app.py index b4909f4f8b..8fa7989b47 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -553,6 +553,19 @@ def startup(self): self.main_window.show() + def _startup(self): + # This is a wrapper around the user's startup method that performs any + # post-setup validation. + self.startup() + self._verify_startup() + + def _verify_startup(self): + 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 about(self): """Display the About dialog for the app. @@ -683,6 +696,10 @@ def __init__( def _create_impl(self): return self.factory.DocumentApp(interface=self) + def _verify_startup(self): + # No post-startup validation required for DocumentApps + pass + @property def documents(self): """Return the list of documents associated with this app. diff --git a/core/tests/test_app.py b/core/tests/test_app.py index 8f8d05fe99..e6ff1b747d 100644 --- a/core/tests/test_app.py +++ b/core/tests/test_app.py @@ -172,6 +172,21 @@ async def test_handler(sender): args=(None,), ) + def test_override_startup(self): + class BadApp(toga.App): + "A startup method that doesn't assign main window raises an error (#760)" + + def startup(self): + # Override startup but don't create a main window + pass + + app = BadApp(app_name="bad_app", formal_name="Bad Aoo", app_id="org.beeware") + with self.assertRaisesRegex( + ValueError, + r"Application does not have a main window.", + ): + app.main_loop() + class DocumentAppTests(TestCase): def setUp(self): @@ -191,3 +206,15 @@ def test_app_documents(self): doc = MagicMock() self.app._documents.append(doc) self.assertEqual(self.app.documents, [doc]) + + def test_override_startup(self): + mock = MagicMock() + + class DocApp(toga.DocumentApp): + def startup(self): + # A document app doesn't have to provide a Main Window. + mock() + + app = DocApp(app_name="docapp", formal_name="Doc App", app_id="org.beeware") + app.main_loop() + mock.assert_called_once() diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 8e84c3c63d..9a5fd67fe4 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -25,6 +25,7 @@ def __init__(self, interface): def create(self): self._action("create") + self.interface._startup() @not_required_on("mobile") def create_menus(self): @@ -32,6 +33,7 @@ def create_menus(self): def main_loop(self): self._action("main loop") + self.create() def set_main_window(self, window): self._set_value("main_window", window) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index af55235acc..5f7dee07c6 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -95,7 +95,7 @@ def gtk_startup(self, data=None): ) self._create_app_commands() - self.interface.startup() + self.interface._startup() # Create the lookup table of menu items, # then force the creation of the menus. diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index c93f301a54..217e14fc51 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -65,7 +65,7 @@ def __init__(self, interface): def create(self): """Calls the startup method on the interface.""" - self.interface.startup() + self.interface._startup() def open_document(self, fileURL): """Add a new document to this app.""" diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 808ca748d5..9e0c646376 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -37,7 +37,7 @@ def create(self): self.create_menus() # Call user code to populate the main window - self.interface.startup() + self.interface._startup() def _create_submenu(self, group, items): submenu = create_element( diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 6af098418f..7a50715fc0 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -129,7 +129,7 @@ def create(self): self._create_app_commands() # Call user code to populate the main window - self.interface.startup() + self.interface._startup() self.create_menus() self.interface.main_window._impl.set_app(self)