Skip to content

fix(web_browser): fall back to scale factor 1 on macOS when AppKit is unavailable#4479

Open
he-yufeng wants to merge 10 commits into
UKGovernmentBEIS:mainfrom
he-yufeng:fix/macos-scale-factor-fallback
Open

fix(web_browser): fall back to scale factor 1 on macOS when AppKit is unavailable#4479
he-yufeng wants to merge 10 commits into
UKGovernmentBEIS:mainfrom
he-yufeng:fix/macos-scale-factor-fallback

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

This PR contains:

  • New features
  • Changes to dev-tools e.g. CI config / github tooling
  • Docs
  • Bug fixes
  • Code refactor

What is the current behavior?

get_screen_scale_factor() (called by the web browser tool when running headful) guards its Windows and Linux branches with try/except and defaults to 1.0, but the macOS branch imports AppKit and dereferences NSScreen.mainScreen() unconditionally. Two reachable cases crash instead of degrading:

  • pyobjc-framework-AppKit is an optional dependency — its own comment notes it's "required to run headfully on macOS" — so a headful run without it raises ModuleNotFoundError (reproduced on macOS with the package absent).
  • NSScreen.mainScreen() returns None when no display is attached (headless / over SSH), so .backingScaleFactor() raises AttributeError even with AppKit installed.

What is the new behavior?

The macOS branch wraps the AppKit import and null-guards mainScreen(), falling back to a scale factor of 1.0 — consistent with the Windows and Linux branches and the function's documented default. Adds unit tests for the working, headless, and missing-AppKit paths.

Does this PR introduce a breaking change?

No.

Other information:

Reproduced on macOS without pyobjc-framework-AppKit: the probe raised ModuleNotFoundError: No module named 'AppKit'; after the fix it returns 1.0. The three new tests cover the working, headless (mainScreen() is None), and missing-import paths.

… unavailable

`get_screen_scale_factor()` guards the Windows and Linux branches with
try/except and defaults to 1.0, but the macOS branch imported AppKit and
dereferenced `NSScreen.mainScreen()` unconditionally. Two reachable cases
crashed instead of degrading:

- `pyobjc-framework-AppKit` is an optional dependency ("required to run
  headfully on macOS"), so a headful run without it raised ImportError.
- `NSScreen.mainScreen()` returns None with no display attached (headless /
  over SSH), so `.backingScaleFactor()` raised AttributeError.

Wrap the import and null-guard the screen so macOS degrades to 1.0 like the
other platforms. Adds unit tests for the working, headless, and missing-AppKit
paths.
@he-yufeng
he-yufeng force-pushed the fix/macos-scale-factor-fallback branch from 24851e6 to 8291f7c Compare July 13, 2026 09:51

@dragonstyle dragonstyle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think that we should catch a ModuleNotFoundError and warn once before we return the default value.

…-factor probe

Per review: catch the missing-AppKit ModuleNotFoundError specifically and log a
one-time warning (pointing at `pip install pyobjc-framework-AppKit`) before
returning the default scale factor of 1, instead of degrading silently. The
darwin probe is extracted into a helper and the warning is gated with
functools.cache so it fires at most once per process.
@he-yufeng

Copy link
Copy Markdown
Contributor Author

Done — the darwin probe now catches the missing-AppKit ModuleNotFoundError specifically and logs a one-time warning (pointing at pip install pyobjc-framework-AppKit) before returning the default scale factor of 1. I pulled the macOS branch into a _darwin_scale_factor helper and gated the warning with functools.cache so it fires at most once per process rather than on every call. Verified the once behavior directly (three consecutive missing-AppKit calls emit a single warning). Also merged main to clear the CHANGELOG conflict.

def _warn_missing_appkit() -> None:
# @cache makes this warn exactly once per process, however many headful macOS
# calls hit the missing optional AppKit dependency.
logger.warning(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You can just use def warn_once(logger: Logger, message: str) -> None:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — swapped the @cache one-off for a warn_once(logger, message) helper.

One wrinkle worth flagging: inspect_tool_support runs inside the sandbox container where inspect_ai isn't importable (there's already a # TODO: cloned from inspect_ai repo code that is unavailable in the container note in _util/json_rpc_helpers.py), so I mirrored the inspect_ai/_util/logger.py warn_once locally rather than importing it. Same signature you suggested.

@dragonstyle dragonstyle self-assigned this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants