Skip to content

Fix mypy type errors and update to iterm2 v2.13 API#4

Merged
mkusaka merged 27 commits into
mainfrom
fix/mypy-iterm2-type-errors
Feb 6, 2026
Merged

Fix mypy type errors and update to iterm2 v2.13 API#4
mkusaka merged 27 commits into
mainfrom
fix/mypy-iterm2-type-errors

Conversation

@mkusaka
Copy link
Copy Markdown
Owner

@mkusaka mkusaka commented Feb 6, 2026

Summary

  • Pin iterm2>=2.13 and update python_version to 3.9 for mypy
  • Fix all mypy type errors across the codebase by using correct iterm2 v2.13 API methods
  • Restore app version and app theme set commands using the Preferences API (PreferenceKey.ITERM_VERSION, PreferenceKey.THEME)
  • Remove commands that relied on non-existent iterm2 APIs
  • Update docs (README.md, IMPLEMENTATION.md) to reflect current commands and Python 3.9+ requirement

Breaking Changes

Removed commands

  • it2 profile create — removed because iterm2.Profile.async_create() does not exist in the iterm2 library
  • it2 app create-hotkey-window — removed because iterm2.HotkeyWindow class does not exist in the iterm2 library

Changed CLI interfaces

  • it2 tab move — previously it2 tab move INDEX [TAB_ID] which moved a tab to a specific index. Now it2 tab move [TAB_ID] which moves the tab to its own new window. The INDEX argument was removed because Tab.async_move_to_window_index() does not exist; Tab.async_move_to_window() is the correct API.
  • it2 app theme — previously accepted light|dark|auto. Now accepts light|dark|light-hc|dark-hc|automatic|minimal (the full set supported by PreferenceKey.THEME). The auto choice is replaced by automatic.
  • it2 app version — previously showed version and build number (both were always None due to non-existent variables). Now shows only the version string via PreferenceKey.ITERM_VERSION. Build number is no longer displayed.

Changes

API fixes (all modules)

  • window.py: Use async_set_fullscreen() / async_get_fullscreen(), async_invoke_function() for arrangements, correct ScreenContents iteration
  • tab.py: Use async_move_to_window() (move to new window), remove non-existent async_move_to_window_index()
  • session.py: Use ScreenContents line-by-line iteration via .line(i).string, correct async_send_text() calls
  • profile.py: Parse font string manually (no .size/.family attrs on normal_font), use async_set_* methods for property changes, fix Color() to use int 0-255
  • app.py: Use Preferences API for version/theme, use MainMenu for hide/quit, use BroadcastDomain correctly
  • monitor.py: Use ScreenStreamer for output monitoring, KeystrokeMonitor(session=) for keystroke monitoring with correct keystroke.characters property
  • connection.py: Use Connection.async_create() external connection pattern
  • config_commands.py: Add proper type annotations

New/restored commands

  • it2 app version — reads iTerm2 version via PreferenceKey.ITERM_VERSION
  • it2 app theme [light|dark|light-hc|dark-hc|automatic|minimal] — show or set theme via PreferenceKey.THEME

Config

  • pyproject.toml: Align ruff/black/mypy target-version to py39

Tests

  • Added test suites: test_monitor_commands.py (5 tests), test_profile_commands.py (5 tests)
  • Updated all existing test mocks to match iterm2 v2.13 API
  • 132 passed, 5 skipped

Test plan

  • pytest — 132 passed, 5 skipped
  • mypy src/it2/ — no errors (17 files checked)
  • black --check src/ tests/ — all formatted
  • ruff check src tests — all passed
  • Manual verification: all safe CLI commands tested against live iTerm2

- Pin iterm2>=2.13 to ensure py.typed marker is available for type checking
- Update mypy python_version from 3.8 to 3.9 (3.8 unsupported by mypy 1.x)
- Remove redundant iterm2.* overrides (py.typed makes ignore_missing_imports
  irrelevant for iterm2)

This enables mypy to detect incorrect iterm2 API usage at build time.
Currently 51 type errors are expected and will be fixed in subsequent commits.
Tests now mock the real iterm2 v2.13 methods instead of non-existent ones:
- iterm2.Window.async_create instead of app.async_create_window
- window.async_get_fullscreen instead of async_is_fullscreen
- window.async_set_fullscreen instead of async_toggle_fullscreen
- iterm2.Arrangement.async_save/restore/list instead of app methods

15 tests now fail against the buggy window.py code, confirming the
tests correctly detect the API mismatch.
Replace non-existent API calls with real iterm2 v2.13 methods:
- app.async_create_window() → iterm2.Window.async_create(connection, ...)
- window.async_is_fullscreen() → window.async_get_fullscreen()
- window.async_toggle_fullscreen() → window.async_set_fullscreen(bool)
- app.async_save_window_arrangement() → iterm2.Arrangement.async_save()
- app.async_list_window_saved_arrangements() → iterm2.Arrangement.async_list()
- app.async_restore_window_arrangement() → iterm2.Arrangement.async_restore()

Also update shortcut tests to mock the correct API for 'it2 new'.
Tab.async_move_to_window_index() doesn't exist in iterm2 v2.13.
The correct API is Tab.async_move_to_window() which moves a tab to
its own new window (no index parameter).

Update tests to reflect the corrected command interface.
4 tests now fail against the buggy tab.py code.
Tab.async_move_to_window_index() doesn't exist in iterm2 v2.13.
Replace with Tab.async_move_to_window() which moves the tab to its
own new window. Remove the index argument since the API doesn't
support index-based tab reordering.
Update test_session_read and test_session_capture mocks to use the
correct ScreenContents API (line(i).string iteration) instead of
the nonexistent string_ignoring_hard_newlines() method.

These tests now fail against the current session.py code, which
still uses the wrong API.
- read: replace nonexistent string_lines_ignoring_hard_newlines() and
  string_ignoring_hard_newlines() with line(i).string iteration
- capture: replace async_get_contents() (no args) with
  async_get_line_info() + async_get_contents(first_line, total) for
  scrollback history; use line(i).string iteration for visible contents
- hide: mock MainMenu.async_select_menu_item instead of app.async_hide
- quit: unskip test, mock MainMenu.async_select_menu_item
- theme: change to test read-only async_get_theme (set not supported)
- broadcast on/off: mock iterm2.async_set_broadcast_domains
- Remove create-hotkey-window test (HotkeyWindow doesn't exist)
- Remove skipped theme_dark/theme_light tests

These tests now fail against the current app.py code.
- hide: use MainMenu.async_select_menu_item instead of nonexistent
  App.async_hide
- quit: use MainMenu.async_select_menu_item instead of nonexistent
  connection.async_send_notification/TerminateAppRequest
- broadcast on/off/add: use module-level
  iterm2.async_set_broadcast_domains with BroadcastDomain instead of
  nonexistent Tab/Session.async_set_broadcast_domains
- theme: change to read-only (async_get_theme) since async_set_theme
  and iterm2.Theme enum don't exist
- Remove create-hotkey-window command (iterm2.HotkeyWindow doesn't
  exist)
- list: verify JSON output
- show: expect normal_font as str with parsed font_size
- set font-size: expect async_set_normal_font call
- set bg-color: expect async_set_background_color with int Color
- create_removed: verify create command is gone (no async_create)

These tests fail against the current profile.py code.
- show: parse normal_font string (e.g. "Monaco 12") instead of
  accessing nonexistent .size/.family attributes
- set: use async_set_* methods (async_set_normal_font,
  async_set_background_color, etc.) instead of setattr + nonexistent
  async_save
- _parse_color: use int 0-255 for Color constructor instead of float
- Remove create command (Profile.async_create doesn't exist)
- output: expect line(i).string iteration instead of
  string_ignoring_hard_newlines
- variable: expect VariableMonitor with 4 args (connection, scope,
  name, identifier)

These tests fail against the current monitor.py code.
- output: replace nonexistent string_ignoring_hard_newlines() with
  line(i).string iteration on ScreenContents
- variable: use VariableMonitor with 4 args (connection, scope, name,
  identifier) and VariableScopes.SESSION enum instead of passing
  Session object as scope
- prompt: use PromptMonitor instead of get_screen_streamer with
  nonexistent want_prompt_notifications param
- activity: add None checks for current_terminal_window chain
- get_app: add assertion to narrow Optional[App] return type
- close: remove nonexistent Connection.async_close() call
- run_until_complete: pass callable (not awaitable) matching expected
  Callable[[Connection], Coroutine] signature
- session_handler.py: check current_terminal_window, current_tab, and
  current_session individually instead of chaining on Optional values
- config_commands.py: add missing None check for current_tab before
  accessing current_session
- window.py: add type: ignore for Window.async_create Optional args
  (upstream typed as str but defaults to None), cast dict values to str
  for Table.add_row
- tab.py: add None check for tab.current_session, cast dict values
  to str for Table.add_row, fix variable shadowing for Optional[Window]
The iTerm2 Python API does not expose version or build number
through its variable system. The global scope only has 3 variables:
applicationPID, localhostName, effectiveTheme. The version command
always returned None for both values.
- Add `app version` command using PreferenceKey.ITERM_VERSION
- Extend `app theme` to accept optional value for setting theme
  via PreferenceKey.THEME (light, dark, light-hc, dark-hc, automatic, minimal)
- Update README.md and IMPLEMENTATION.md to reflect current commands
  (remove profile create, fix theme examples, update test counts,
  correct Python version requirement to 3.9+)
- Remove unused _THEME_NAMES mapping (app.py)
- Fix keystroke monitor: use KeystrokeMonitor(session=) for filtering
  and keystroke.characters instead of non-existent .session/.keystrokes
- Align ruff/black target-version to py39 (matching mypy)
- Add keystroke monitor tests (with pattern filtering)
- Add monitor output pattern filter test
Update deprecated typing imports (List→list, Dict→dict, Tuple→tuple,
Pattern→re.Pattern, Awaitable→collections.abc.Awaitable), fix import
sorting, add pytest fixture/mark parentheses, and ignore S603 for
session.py's hardcoded /usr/bin/pbcopy subprocess call.
Previous commit added parentheses to @pytest.fixture() and
@pytest.mark.asyncio() based on local ruff 0.5.5, but the lockfile
pins ruff 0.12.3 which requires the opposite (no parentheses).
Synced local env and re-ran auto-fix with the correct version.
Click option --all maps to parameter name "all" by default, but
the function expected "all_sessions". This caused a TypeError that
was swallowed by run_command's broad except, showing a misleading
"not running inside iTerm2" error. Add explicit parameter name
"all_sessions" to the click.option decorator.
int(font_size) truncated fractional sizes (e.g. 13.5 → 13) when
changing font family. Use the float value directly to preserve the
original size.

Also add supported property list to `profile set --help`.
- window.py: improve type: ignore comment with specific reasoning
  (iterm2 stubs type profile/command as str but implementation accepts None)
- connection.py: replace assert with explicit RuntimeError raise
  (assert is stripped with -O flag)
- profile.py: _parse_font_string now raises ValueError instead of
  returning 0.0 fallback that could silently set font size to zero;
  show command gracefully handles parse failures
- app.py: document that broadcast on/add replace all existing
  broadcast domains (API constraint)
async_create has `profile: str = None` instead of `Optional[str] = None`.
@mkusaka mkusaka merged commit 609df56 into main Feb 6, 2026
7 checks passed
@mkusaka mkusaka deleted the fix/mypy-iterm2-type-errors branch February 6, 2026 10:21
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.

1 participant