diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 19f86ba91..662300f16 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,6 +11,9 @@ jobs: # This permission is required for trusted publishing. id-token: write strategy: + # One element of this matrix failing should not terminate the others mid-run. + # This prevents one bad platform from stalling the publication of others. + fail-fast: false matrix: package: - "toga" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51109da22..89fa2e056 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,11 @@ jobs: permissions: # This permission is required for trusted publishing. id-token: write + continue-on-error: true strategy: + # One element of this matrix failing should not terminate the others mid-run. + # This prevents one bad platform from stalling the publication of others. + fail-fast: false matrix: package: - "toga" diff --git a/changes/2557.misc.rst b/changes/2557.misc.rst deleted file mode 100644 index 95e96eaa7..000000000 --- a/changes/2557.misc.rst +++ /dev/null @@ -1 +0,0 @@ -GTK and Windows MapViews have maxZoom of 20, as per the docs. diff --git a/cocoa/src/toga_cocoa/icons.py b/cocoa/src/toga_cocoa/icons.py index d02816196..bda0965f9 100644 --- a/cocoa/src/toga_cocoa/icons.py +++ b/cocoa/src/toga_cocoa/icons.py @@ -16,13 +16,20 @@ class Icon: if path is None: # Look to the app bundle, and get the icon. Set self.path as None # as an indicator that this is the app's default icon. + # This bundle icon file definition might not contain an extension, + # even thought the actual file will; so force the .icns extension. bundle_icon = Path( NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleIconFile") ) path = NSBundle.mainBundle.pathForResource( bundle_icon.stem, - ofType=bundle_icon.suffix, + ofType=".icns", ) + # If the icon file doesn't exist, raise the problem as FileNotFoundError + # This can't be tested, as the app will always have an icon. + if not Path(path).is_file(): + raise FileNotFoundError() # pragma: no cover + self.path = None else: self.path = path diff --git a/core/src/toga/icons.py b/core/src/toga/icons.py index 6e7b771ba..740155b49 100644 --- a/core/src/toga/icons.py +++ b/core/src/toga/icons.py @@ -91,11 +91,12 @@ class Icon: path, or a path relative to the module that defines your Toga application class. This base filename should *not* contain an extension. If an extension is specified, it will be ignored. If the icon cannot be found, the default - icon will be :attr:`~toga.Icon.DEFAULT_ICON`. + icon will be :attr:`~toga.Icon.DEFAULT_ICON`. If an icon file is found, but + it cannot be loaded (due to a file format or permission error), an exception + will be raised. :param system: **For internal use only** """ self.factory = get_platform_factory() - try: # Try to load the icon with the given path snippet. If the request is for the # app icon, use ``resources/`` as the path. @@ -131,7 +132,7 @@ class Icon: self._impl = self.factory.Icon(interface=self, path=full_path) except FileNotFoundError: - # Icon path couldn't be loaded. If the path is the sentinel for the app + # Icon path couldn't be found. If the path is the sentinel for the app # icon, and this isn't running as a script, fall back to the application # binary if path is _APP_ICON: diff --git a/core/tests/test_icons.py b/core/tests/test_icons.py index f3773e2b7..d5095ad36 100644 --- a/core/tests/test_icons.py +++ b/core/tests/test_icons.py @@ -113,8 +113,15 @@ def test_create(monkeypatch, app, path, system, sizes, extensions, final_paths): assert icon._impl.path == final_paths -def test_create_fallback(app, capsys): +def test_create_fallback_missing(monkeypatch, app, capsys): """If a resource doesn't exist, a fallback icon is used.""" + # Prime the dummy so the app icon cannot be loaded + monkeypatch.setattr( + DummyIcon, + "ICON_FAILURE", + FileNotFoundError(), + ) + icon = toga.Icon("resources/missing") assert icon._impl is not None @@ -127,6 +134,19 @@ def test_create_fallback(app, capsys): ) +def test_create_fallback_unloadable(monkeypatch, app, capsys): + """If a resource exists, but can't be loaded, an error is raised.""" + # Prime the dummy so the app icon cannot be loaded + monkeypatch.setattr( + DummyIcon, + "ICON_FAILURE", + ValueError("Icon could not be loaded"), + ) + + with pytest.raises(ValueError): + toga.Icon("resources/sample") + + def test_create_fallback_variants(monkeypatch, app, capsys): """If a resource with size variants doesn't exist, a fallback icon is used.""" monkeypatch.setattr(DummyIcon, "SIZES", [32, 72]) @@ -164,7 +184,7 @@ def test_create_app_icon(monkeypatch, app, capsys): def test_create_app_icon_missing(monkeypatch, app, capsys): - """The app icon can be constructed""" + """If the app icon is missing, a fallback is used""" # When running under pytest, code will identify as running as a script # Load the app default icon. @@ -196,9 +216,13 @@ def test_create_app_icon_non_script(monkeypatch, app, capsys): def test_create_app_icon_missing_non_script(monkeypatch, app, capsys): - """The binary executableThe app icon can be reset to the default""" - # Prime the dummy so the app icon cannot be loaded - monkeypatch.setattr(DummyIcon, "ICON_EXISTS", False) + """If the icon from binary executable cannot be found, the app icon is reset to the default""" + # Prime the dummy so the app icon cannot be found + monkeypatch.setattr( + DummyIcon, + "ICON_FAILURE", + FileNotFoundError(), + ) # Patch sys.executable so the test looks like it's running as a packaged binary monkeypatch.setattr(sys, "executable", "/path/to/App") diff --git a/docs/background/project/releases.rst b/docs/background/project/releases.rst index ba5a45b2d..8202ed893 100644 --- a/docs/background/project/releases.rst +++ b/docs/background/project/releases.rst @@ -6,6 +6,19 @@ Release History .. towncrier release notes start +0.4.4 (2024-05-08) +================== + +Bugfixes +-------- + +* The mechanism for loading application icons on macOS was corrected to account for how Xcode populates ``Info.plist`` metadata. (`#2558 `__) + +Misc +---- + +* `#2555 `__, `#2557 `__, `#2560 `__ + 0.4.3 (2024-05-06) ================== @@ -23,7 +36,6 @@ Features * A geolocation service was added for Android, iOS and macOS. (`#2462 `__) * When a Toga app is packaged as a binary, and no icon is explicitly configured, Toga will now use the binary's icon as the app icon. This means it is no longer necessary to include the app icon as data in a ``resources`` folder if you are packaging your app for distribution. (`#2527 `__) - Bugfixes -------- @@ -40,13 +52,11 @@ Bugfixes * Widget IDs can now be reused after the associated widget's window is closed. (`#2514 `__) * :class:`~toga.WebView` is now compatible with Linux GTK environments only providing WebKit2 version 4.1 without version 4.0. (`#2527 `__) - Backward Incompatible Changes ----------------------------- * The macOS implementations of ``Window.as_image()`` and ``Canvas.as_image()`` APIs now return images in native device resolution, not CSS pixel resolution. This will result in images that are double the previous size on Retina displays. (`#1930 `__) - Documentation ------------- @@ -58,13 +68,11 @@ Documentation * An explicit system requirements section was added to the documentation for widgets that require the installation of additional system components. (`#2544 `__) * The system requirements were updated to be more explicit and now include details for OpenSUSE Tumbleweed. (`#2549 `__) - Misc ---- * `#2153 `__, `#2372 `__, `#2389 `__, `#2390 `__, `#2391 `__, `#2392 `__, `#2393 `__, `#2394 `__, `#2396 `__, `#2397 `__, `#2400 `__, `#2403 `__, `#2405 `__, `#2406 `__, `#2407 `__, `#2408 `__, `#2409 `__, `#2422 `__, `#2423 `__, `#2427 `__, `#2440 `__, `#2442 `__, `#2445 `__, `#2448 `__, `#2449 `__, `#2450 `__, `#2457 `__, `#2458 `__, `#2459 `__, `#2460 `__, `#2464 `__, `#2465 `__, `#2466 `__, `#2467 `__, `#2470 `__, `#2471 `__, `#2476 `__, `#2487 `__, `#2488 `__, `#2498 `__, `#2501 `__, `#2502 `__, `#2503 `__, `#2504 `__, `#2509 `__, `#2518 `__, `#2519 `__, `#2520 `__, `#2521 `__, `#2522 `__, `#2523 `__, `#2532 `__, `#2533 `__, `#2534 `__, `#2535 `__, `#2536 `__, `#2537 `__, `#2538 `__, `#2539 `__, `#2540 `__, `#2541 `__, `#2542 `__, `#2546 `__, `#2552 `__ - 0.4.2 (2024-02-06) ================== @@ -106,7 +114,6 @@ Misc * `#2298 `__, `#2299 `__, `#2302 `__, `#2312 `__, `#2313 `__, `#2318 `__, `#2331 `__, `#2332 `__, `#2333 `__, `#2336 `__, `#2337 `__, `#2339 `__, `#2340 `__, `#2357 `__, `#2358 `__, `#2359 `__, `#2363 `__, `#2367 `__, `#2368 `__, `#2369 `__, `#2370 `__, `#2371 `__, `#2375 `__, `#2376 `__ - 0.4.1 (2023-12-21) ================== @@ -319,7 +326,6 @@ Features * The Web backend now uses Shoelace to provide web components. (`#1838 `__) * Winforms apps can now go full screen. (`#1863 `__) - Bugfixes -------- @@ -332,7 +338,6 @@ Bugfixes * The text alignment of MultilineTextInput on Android has been fixed to be TOP aligned. (`#1808 `__) * GTK widgets that involve animation (such as Switch or ProgressBar) are now redrawn correctly. (`#1826 `__) - Improved Documentation ---------------------- @@ -340,13 +345,11 @@ Improved Documentation * Some missing settings and constant values were added to the documentation of Pack. (`#1786 `__) * Added documentation for ``toga.App.widgets``. (`#1852 `__) - Misc ---- * `#1750 `__, `#1764 `__, `#1765 `__, `#1766 `__, `#1770 `__, `#1771 `__, `#1777 `__, `#1797 `__, `#1802 `__, `#1813 `__, `#1818 `__, `#1822 `__, `#1829 `__, `#1830 `__, `#1835 `__, `#1839 `__, `#1854 `__, `#1861 `__ - 0.3.0 (2023-01-30) ================== diff --git a/docs/reference/api/resources/icons.rst b/docs/reference/api/resources/icons.rst index 2358149a4..90cdfad22 100644 --- a/docs/reference/api/resources/icons.rst +++ b/docs/reference/api/resources/icons.rst @@ -71,9 +71,11 @@ icon variants that are not available from the highest resolution provided (e.g., 128px variant can be found, one will be created by scaling the highest resolution variant that *is* available). -An icon is **guaranteed** to have an implementation, regardless of the path -specified. If you specify a path and no matching icon can be found, Toga will -output a warning to the console, and return :attr:`~toga.Icon.DEFAULT_ICON`. +An icon is **guaranteed** to have an implementation, regardless of the path specified. +If you specify a path and no matching icon can be found, Toga will output a warning to +the console, and return :attr:`~toga.Icon.DEFAULT_ICON`. The only exception to this is +if an icon file is *found*, but it cannot be loaded (e.g., due to a file format or +permission error). In this case, an error will be raised. Reference --------- diff --git a/docs/reference/api/widgets/table.rst b/docs/reference/api/widgets/table.rst index 5257fc0e9..66f6c8dfb 100644 --- a/docs/reference/api/widgets/table.rst +++ b/docs/reference/api/widgets/table.rst @@ -82,7 +82,7 @@ to control the display order of columns independent of the storage of that data. headings=["Name", "Age"], data=[ {"name": "Arthur Dent", "age": 42, "planet": "Earth"}, - {"name", "Ford Prefect", "age": 37, "planet": "Betelgeuse Five"}, + {"name": "Ford Prefect", "age": 37, "planet": "Betelgeuse Five"}, {"name": "Tricia McMillan", "age": 38, "planet": "Earth"}, ] ) @@ -106,7 +106,7 @@ header, but internally, the attribute "character" will be used: accessors={"Name", 'character'}, data=[ {"character": "Arthur Dent", "age": 42, "planet": "Earth"}, - {"character", "Ford Prefect", "age": 37, "planet": "Betelgeuse Five"}, + {"character": "Ford Prefect", "age": 37, "planet": "Betelgeuse Five"}, {"name": "Tricia McMillan", "age": 38, "planet": "Earth"}, ] ) diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 206a9dc8c..743105bec 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -99,4 +99,5 @@ Wayland WebKit whitespace Winforms +Xcode zoomable diff --git a/dummy/src/toga_dummy/icons.py b/dummy/src/toga_dummy/icons.py index 0f6f656a9..1a27be714 100644 --- a/dummy/src/toga_dummy/icons.py +++ b/dummy/src/toga_dummy/icons.py @@ -2,18 +2,19 @@ from .utils import LoggedObject class Icon(LoggedObject): - ICON_EXISTS = True + ICON_FAILURE = None EXTENSIONS = [".png", ".ico"] SIZES = None def __init__(self, interface, path): super().__init__() self.interface = interface - if not self.ICON_EXISTS: - raise FileNotFoundError("Couldn't find icon") - elif path is None: - self.path = "" - elif path == {}: - raise FileNotFoundError("No image variants found") + if self.ICON_FAILURE: + raise self.ICON_FAILURE else: - self.path = path + if path is None: + self.path = "" + elif path == {}: + raise FileNotFoundError("No image variants found") + else: + self.path = path