diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df0b3b2..d0999d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: - "docs/**" - ".readthedocs.yaml" - "pyproject.toml" - - "setup.cfg" + - "mypy.ini" - "**/*.py" pull_request: paths: @@ -28,9 +28,16 @@ concurrency: cancel-in-progress: true jobs: + ruff: + runs-on: ubuntu-latest + timeout-minutes: &timeout-minutes 5 + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/ruff-action@v4.0.0 + mypy: runs-on: ${{ matrix.os }} - timeout-minutes: &timeout-minutes 5 + timeout-minutes: *timeout-minutes strategy: # mypy is os and python-version sensitive. Test on all supported combinations matrix: diff --git a/mypy.ini b/mypy.ini index 3ea79a5..9a93867 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,6 @@ [mypy] mypy_path = src/, typings/ -exclude = build +exclude = build/, dist/ strict = True # Leverage type inference for function return type @@ -8,10 +8,6 @@ disallow_untyped_calls = False disallow_incomplete_defs = False disallow_untyped_defs = False -# https://github.com/python/mypy/issues/8234 (post assert "type: ignore") -# https://github.com/python/mypy/issues/8823 (version specific "type: ignore") -warn_unused_ignores = False - disable_error_code = # https://github.com/python/mypy/issues/6232 (redefinition with correct type) attr-defined, assignment, diff --git a/pyproject.toml b/pyproject.toml index 82c261a..2a56331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ dev = [ { include-group = "docs" }, "ewmhlib", "mypy>=0.990,<2", + "ruff>=0.15.16", "types-python-xlib>=0.32", "types-pywin32>=305.0.0.3", ] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..08f38de --- /dev/null +++ b/ruff.toml @@ -0,0 +1,75 @@ +[lint] +future-annotations = true +# https://docs.astral.sh/ruff/rules/ +extend-select = [ + "ANN2", # flake8-annotations: missing-return-type + "PYI", # flake8-pyi + "FA", # flake8-future-annotations + "ICN", # flake8-import-conventions + "F401", # unused-import + "YTT", # flake8-2020 + "TC", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "RUF", # Ruff-specific rules + "F404", # late-future-import + "PGH", # pygrep-hooks (blanket-* rules) +] +ignore = [ + # Only enforce return types on public functions. Where otherwise mypy infers as Any + # Still worth running `ruff check --fix --select=202` once in a while for autofixes + "ANN202", # missing-return-type-private-function + # Explicit is preferred + "UP015", # redundant-open-modes, + # Autofixes print-f style formatting to f-strings, + # which is sometimes simpler, but looses template code reading semantics + "UP032", # f-string + # TC helps prevent circular imports, reduce runtime cost of typing symbols, + # and prevent leaking implementations details into modules + # However stdlib is not at risk of circular import, is clearly not public API, + # and assume it's gonna be included in the import chain at some point anyway + "TC003", # typing-only-standard-library-import + # Typeshed doesn't want complex or non-literal defaults for maintenance and testing reasons. + # This doesn't affect us, let's have more complete stubs. + "PYI011", # typed-argument-default-in-stub + "PYI014", # argument-default-in-stub + "PYI053", # string-or-bytes-too-long + + # TODO: Consider later + "UP031", # printf-string-formatting + "RUF059", # unused-unpacked-variable + "E722", # bare-except +] +# F401 would remove imports not marked as explicit re-exports, which may break API boundaries +extend-unsafe-fixes = ["F401"] + +[lint.per-file-ignores] +"**/typings/**/*.pyi" = [ + "PGH003", # TODO: Blanket ignores until using pyobj type stubs + "F811", # Re-exports false positives + # The following can't be controlled for external libraries: + "A", # Shadowing builtin names + "E741", # ambiguous variable name + "F403", # `from . import *` used; unable to detect undefined names + "FBT", # flake8-boolean-trap + "ICN001", # unconventional-import-alias + "N8", # Naming conventions + "PLC2701", # Private name import + "PLE0302", # The special method expects a given signature + "PLR0904", # Too many public methods + "PLR0913", # Argument count + "PLR0917", # Too many positional arguments + "PLW3201", # misspelled dunder method name + "SLOT", # flake8-slots + # Stubs can sometimes re-export entire modules. + # Issues with using a star-imported name will be caught by type-checkers. + "F405", # may be undefined, or defined from star imports + # It's normal to be missing annotations for local stubs. + # If they were complete, we'd upload them to typeshed! + "ANN0", + "ANN2", +] + +# https://docs.astral.sh/ruff/settings/#lintflake8-type-checking +[lint.flake8-type-checking] +quote-annotations = true diff --git a/src/pywinctl/__init__.py b/src/pywinctl/__init__.py index 09c3035..0615a18 100644 --- a/src/pywinctl/__init__.py +++ b/src/pywinctl/__init__.py @@ -1,7 +1,12 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- +from ._main import (Re, Window, checkPermissions, getActiveWindow, + getActiveWindowTitle, getAllAppsNames, getAllAppsWindowsTitles, + getAllTitles, getAllWindows, getAppsWithName, getWindowsWithTitle, + getAllWindowsDict, getTopWindowAt, getWindowsAt, displayWindowsUnderMouse, + getAllScreens, getScreenSize, getWorkArea, getMousePos + ) -__all__ = [ +__all__ = [ # noqa: RUF022 "version", "Re", # OS Specifics "Window", "checkPermissions", "getActiveWindow", "getActiveWindowTitle", "getWindowsWithTitle", @@ -18,9 +23,3 @@ def version(numberOnly: bool = True) -> str: return ("" if numberOnly else "PyWinCtl-")+__version__ -from ._main import (Re, Window, checkPermissions, getActiveWindow, - getActiveWindowTitle, getAllAppsNames, getAllAppsWindowsTitles, - getAllTitles, getAllWindows, getAppsWithName, getWindowsWithTitle, - getAllWindowsDict, getTopWindowAt, getWindowsAt, displayWindowsUnderMouse, - getAllScreens, getScreenSize, getWorkArea, getMousePos - ) diff --git a/src/pywinctl/_main.py b/src/pywinctl/_main.py index 82d0f7a..93019d4 100644 --- a/src/pywinctl/_main.py +++ b/src/pywinctl/_main.py @@ -1,5 +1,4 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- from __future__ import annotations import difflib @@ -9,18 +8,19 @@ import time from abc import ABC, abstractmethod from collections.abc import Callable -from typing import Any, cast, List, Tuple, Union, TypedDict +from typing import Any, ClassVar, TypedDict -from pymonctl import findMonitorsAtPoint, getAllMonitors, getAllMonitorsDict, getMousePos as getMouse -from pywinbox import PyWinBox, Box, Rect, Point, Size +from pymonctl import findMonitorsAtPoint, getAllMonitors, getAllMonitorsDict +from pymonctl import getMousePos as getMouse +from pywinbox import Box, Point, PyWinBox, Rect, Size class BaseWindow(ABC): - def __init__(self, handle): + def __init__(self, handle) -> None: self._box: PyWinBox = PyWinBox(None, None, handle) - def __str__(self): + def __str__(self) -> str: box = self._box.box return '<%s left="%s", top="%s", width="%s", height="%s", title="%s">' % ( self.__class__.__qualname__, @@ -92,7 +92,7 @@ def moveTo(self, newLeft: int, newTop: int, wait: bool = False) -> bool: raise NotImplementedError @abstractmethod - def getExtraFrameSize(self, includeBorder: bool = True) -> Tuple[int, int, int, int]: + def getExtraFrameSize(self, includeBorder: bool = True) -> tuple[int, int, int, int]: """ Get the extra space, in pixels, around the window, including or not the border. Notice not all applications/windows will use this property values @@ -103,7 +103,7 @@ def getExtraFrameSize(self, includeBorder: bool = True) -> Tuple[int, int, int, raise NotImplementedError @abstractmethod - def getClientFrame(self) -> Tuple[int, int, int, int]: + def getClientFrame(self) -> tuple[int, int, int, int]: """ Get the client area of window, as a Rect (x, y, right, bottom) Notice that scroll and status bars might be included, or not, depending on the application @@ -181,7 +181,7 @@ def setParent(self, parent) -> bool: raise NotImplementedError @abstractmethod - def getChildren(self) -> List[Any]: + def getChildren(self) -> list[Any]: """ Get the children handles of current window @@ -212,7 +212,7 @@ def isChild(self, parent: Any) -> bool: isChildOf = isChild # isParentOf is an alias of isParent method @abstractmethod - def getDisplay(self) -> List[str]: + def getDisplay(self) -> list[str]: """Returns the list of names of the monitors the window is in""" raise NotImplementedError getMonitor = getDisplay # getMonitor is an alias of getDisplay method @@ -252,7 +252,7 @@ def updatedTitle(self) -> str: @abstractmethod def visible(self) -> bool: raise NotImplementedError - isVisible: bool = cast(bool, visible) # isVisible is an alias for the visible property. + isVisible = visible # isVisible is an alias for the visible property. @property @abstractmethod @@ -317,7 +317,7 @@ def position(self) -> Point: return self._box.position @position.setter - def position(self, value: Union[Point, Tuple[int, int]]): + def position(self, value: Point | tuple[int, int]): self._box.position = value @property @@ -325,7 +325,7 @@ def size(self) -> Size: return self._box.size @size.setter - def size(self, value: Union[Size, Tuple[int, int]]): + def size(self, value: Size | tuple[int, int]): self._box.size = value @property @@ -333,7 +333,7 @@ def box(self) -> Box: return self._box.box @box.setter - def box(self, value: Union[Box, Tuple[int, int, int, int]]): + def box(self, value: Box | tuple[int, int, int, int]): self._box.box = value @property @@ -341,7 +341,7 @@ def rect(self) -> Rect: return self._box.rect @rect.setter - def rect(self, value: Union[Rect, Tuple[int, int, int, int]]): + def rect(self, value: Rect | tuple[int, int, int, int]): self._box.rect = value bbox = rect @@ -350,7 +350,7 @@ def topleft(self) -> Point: return self._box.topleft @topleft.setter - def topleft(self, value: Union[Point, Tuple[int, int]]): + def topleft(self, value: Point | tuple[int, int]): self._box.topleft = value @property @@ -358,7 +358,7 @@ def bottomleft(self) -> Point: return self._box.bottomleft @bottomleft.setter - def bottomleft(self, value: Union[Point, Tuple[int, int]]): + def bottomleft(self, value: Point | tuple[int, int]): self._box.bottomleft = value @property @@ -366,7 +366,7 @@ def topright(self) -> Point: return self._box.topright @topright.setter - def topright(self, value: Union[Point, Tuple[int, int]]): + def topright(self, value: Point | tuple[int, int]): self._box.topright = value @property @@ -374,7 +374,7 @@ def bottomright(self) -> Point: return self._box.bottomright @bottomright.setter - def bottomright(self, value: Union[Point, Tuple[int, int]]): + def bottomright(self, value: Point | tuple[int, int]): self._box.bottomright = value @property @@ -382,7 +382,7 @@ def midtop(self) -> Point: return self._box.midtop @midtop.setter - def midtop(self, value: Union[Point, Tuple[int, int]]): + def midtop(self, value: Point | tuple[int, int]): self._box.midtop = value @property @@ -390,7 +390,7 @@ def midbottom(self) -> Point: return self._box.midbottom @midbottom.setter - def midbottom(self, value: Union[Point, Tuple[int, int]]): + def midbottom(self, value: Point | tuple[int, int]): self._box.midbottom = value @property @@ -398,7 +398,7 @@ def midleft(self) -> Point: return self._box.midleft @midleft.setter - def midleft(self, value: Union[Point, Tuple[int, int]]): + def midleft(self, value: Point | tuple[int, int]): self._box.midleft = value @property @@ -406,7 +406,7 @@ def midright(self) -> Point: return self._box.midright @midright.setter - def midright(self, value: Union[Point, Tuple[int, int]]): + def midright(self, value: Point | tuple[int, int]): self._box.midright = value @property @@ -414,7 +414,7 @@ def center(self) -> Point: return self._box.center @center.setter - def center(self, value: Union[Point, Tuple[int, int]]): + def center(self, value: Point | tuple[int, int]): self._box.center = value @property @@ -449,7 +449,7 @@ class _WatchDog: :meth kill: Stop the entire watchdog and all its hooks :meth isAlive: Check if watchdog is running """ - def __init__(self, parent: BaseWindow): + def __init__(self, parent: BaseWindow) -> None: self._watchdog: _WatchDogWorker | None = None self._parent = parent @@ -460,10 +460,10 @@ def start( isVisibleCB: Callable[[bool], None] | None = None, isMinimizedCB: Callable[[bool], None] | None = None, isMaximizedCB: Callable[[bool], None] | None = None, - resizedCB: Callable[[Tuple[int, int]], None] | None = None, - movedCB: Callable[[Tuple[int, int]], None] | None = None, + resizedCB: Callable[[tuple[int, int]], None] | None = None, + movedCB: Callable[[tuple[int, int]], None] | None = None, changedTitleCB: Callable[[str], None] | None = None, - changedDisplayCB: Callable[[List[str]], None] | None = None, + changedDisplayCB: Callable[[list[str]], None] | None = None, interval: float = 0.3 ): """ @@ -515,10 +515,10 @@ def updateCallbacks( isVisibleCB: Callable[[bool], None] | None = None, isMinimizedCB: Callable[[bool], None] | None = None, isMaximizedCB: Callable[[bool], None] | None = None, - resizedCB: Callable[[Tuple[int, int]], None] | None = None, - movedCB: Callable[[Tuple[int, int]], None] | None = None, + resizedCB: Callable[[tuple[int, int]], None] | None = None, + movedCB: Callable[[tuple[int, int]], None] | None = None, changedTitleCB: Callable[[str], None] | None = None, - changedDisplayCB: Callable[[List[str]], None] | None = None + changedDisplayCB: Callable[[list[str]], None] | None = None ): """ Change the states this watchdog is hooked to @@ -606,12 +606,12 @@ def __init__( isVisibleCB: Callable[[bool], None] | None = None, isMinimizedCB: Callable[[bool], None] | None = None, isMaximizedCB: Callable[[bool], None] | None = None, - resizedCB: Callable[[Tuple[int, int]], None] | None = None, - movedCB: Callable[[Tuple[int, int]], None] | None = None, + resizedCB: Callable[[tuple[int, int]], None] | None = None, + movedCB: Callable[[tuple[int, int]], None] | None = None, changedTitleCB: Callable[[str], None] | None = None, - changedDisplayCB: Callable[[List[str]], None] | None = None, + changedDisplayCB: Callable[[list[str]], None] | None = None, interval: float = 0.3 - ): + ) -> None: threading.Thread.__init__(self) self._win = win self._interval = interval @@ -707,7 +707,7 @@ def run(self): visible = self._win.isVisible if self._isVisible != visible: self._isVisible = visible - self._isVisibleCB(visible) # type: ignore[arg-type] # mypy bug + self._isVisibleCB(visible) if self._isMinimizedCB: minimized = self._win.isMinimized @@ -757,10 +757,10 @@ def updateCallbacks( isVisibleCB: Callable[[bool], None] | None = None, isMinimizedCB: Callable[[bool], None] | None = None, isMaximizedCB: Callable[[bool], None] | None = None, - resizedCB: Callable[[Tuple[int, int]], None] | None = None, - movedCB: Callable[[Tuple[int, int]], None] | None = None, + resizedCB: Callable[[tuple[int, int]], None] | None = None, + movedCB: Callable[[tuple[int, int]], None] | None = None, changedTitleCB: Callable[[str], None] | None = None, - changedDisplayCB: Callable[[List[str]], None] | None = None + changedDisplayCB: Callable[[list[str]], None] | None = None ): self._isAliveCB = isAliveCB @@ -792,10 +792,10 @@ def restart( isVisibleCB: Callable[[bool], None] | None = None, isMinimizedCB: Callable[[bool], None] | None = None, isMaximizedCB: Callable[[bool], None] | None = None, - resizedCB: Callable[[Tuple[int, int]], None] | None = None, - movedCB: Callable[[Tuple[int, int]], None] | None = None, + resizedCB: Callable[[tuple[int, int]], None] | None = None, + movedCB: Callable[[tuple[int, int]], None] | None = None, changedTitleCB: Callable[[str], None] | None = None, - changedDisplayCB: Callable[[List[str]], None] | None = None, + changedDisplayCB: Callable[[list[str]], None] | None = None, interval: float = 0.3 ): self._kill.set() @@ -805,7 +805,7 @@ def restart( self.run() -def _findMonitorName(x: int, y: int) -> List[str]: +def _findMonitorName(x: int, y: int) -> list[str]: return [monitor.name for monitor in findMonitorsAtPoint(x, y)] @@ -827,7 +827,8 @@ class Re: IGNORECASE = re.IGNORECASE # Does not play well with static typing and current implementation of TypedDict - _cond_dic: dict[int, Callable[[str | re.Pattern[str], str, float], bool]] = { + # ruff: disable[PGH003] + _cond_dic: ClassVar[dict[int, Callable[[str | re.Pattern[str], str, float], bool]]] = { IS: lambda s1, s2, fl: s1 == s2, CONTAINS: lambda s1, s2, fl: s1 in s2, # type: ignore # pyright: ignore STARTSWITH: lambda s1, s2, fl: s2.startswith(s1), # type: ignore # pyright: ignore @@ -839,9 +840,9 @@ class Re: MATCH: lambda s1, s2, fl: bool(s1.search(s2)), # type: ignore # pyright: ignore NOTMATCH: lambda s1, s2, fl: not (bool(s1.search(s2))), # type: ignore # pyright: ignore EDITDISTANCE: lambda s1, s2, fl: _levenshtein(s1, s2) >= fl, # type: ignore # pyright: ignore - DIFFRATIO: lambda s1, s2, fl: difflib.SequenceMatcher(None, s1, s2).ratio() * 100 >= fl # type: ignore # pyright: ignore + DIFFRATIO: lambda s1, s2, fl: difflib.SequenceMatcher(None, s1, s2).ratio() * 100 >= fl, # type: ignore # pyright: ignore } - + # ruff: enable[PGH003] def _levenshtein(seq1: str, seq2: str) -> float: # https://stackabuse.com/levenshtein-distance-and-text-similarity-in-python/ @@ -994,38 +995,63 @@ def displayWindowsUnderMouse(xOffset: int = 0, yOffset: int = 0) -> None: class _WINDATA(TypedDict): - id: Union[int, tuple[str, str]] + id: int | tuple[str, str] display: list[str] position: tuple[int, int] size: tuple[int, int] status: int -class _WINDICT(TypedDict): +class _WINDICT(TypedDict): # noqa: PYI049 # Private symbol imported by internal modules pid: int windows: dict[str, _WINDATA] - +# Explicit re-exports if sys.platform == "darwin": - from ._pywinctl_macos import (MacOSWindow as Window, checkPermissions, getActiveWindow, - getActiveWindowTitle, getAllAppsNames, getAllAppsWindowsTitles, - getAllTitles, getAllWindows, getAppsWithName, getWindowsWithTitle, - getAllWindowsDict, getTopWindowAt, getWindowsAt - ) - + from ._pywinctl_macos import MacOSWindow as Window + from ._pywinctl_macos import checkPermissions as checkPermissions + from ._pywinctl_macos import getActiveWindow as getActiveWindow + from ._pywinctl_macos import getActiveWindowTitle as getActiveWindowTitle + from ._pywinctl_macos import getAllAppsNames as getAllAppsNames + from ._pywinctl_macos import getAllAppsWindowsTitles as getAllAppsWindowsTitles + from ._pywinctl_macos import getAllTitles as getAllTitles + from ._pywinctl_macos import getAllWindows as getAllWindows + from ._pywinctl_macos import getAllWindowsDict as getAllWindowsDict + from ._pywinctl_macos import getAppsWithName as getAppsWithName + from ._pywinctl_macos import getTopWindowAt as getTopWindowAt + from ._pywinctl_macos import getWindowsAt as getWindowsAt + from ._pywinctl_macos import getWindowsWithTitle as getWindowsWithTitle elif sys.platform == "win32": - from ._pywinctl_win import (Win32Window as Window, checkPermissions, getActiveWindow, - getActiveWindowTitle, getAllAppsNames, getAllAppsWindowsTitles, - getAllTitles, getAllWindows, getAppsWithName, getWindowsWithTitle, - getAllWindowsDict, getTopWindowAt, getWindowsAt - ) - + from ._pywinctl_win import Win32Window as Window + from ._pywinctl_win import checkPermissions as checkPermissions + from ._pywinctl_win import getActiveWindow as getActiveWindow + from ._pywinctl_win import getActiveWindowTitle as getActiveWindowTitle + from ._pywinctl_win import getAllAppsNames as getAllAppsNames + from ._pywinctl_win import getAllAppsWindowsTitles as getAllAppsWindowsTitles + from ._pywinctl_win import getAllTitles as getAllTitles + from ._pywinctl_win import getAllWindows as getAllWindows + from ._pywinctl_win import getAllWindowsDict as getAllWindowsDict + from ._pywinctl_win import getAppsWithName as getAppsWithName + from ._pywinctl_win import getTopWindowAt as getTopWindowAt + from ._pywinctl_win import getWindowsAt as getWindowsAt + from ._pywinctl_win import getWindowsWithTitle as getWindowsWithTitle elif sys.platform == "linux": - from ._pywinctl_linux import (LinuxWindow as Window, checkPermissions, getActiveWindow, - getActiveWindowTitle, getAllAppsNames, getAllAppsWindowsTitles, - getAllTitles, getAllWindows, getAppsWithName, getWindowsWithTitle, - getAllWindowsDict, getTopWindowAt, getWindowsAt - ) - + from ._pywinctl_linux import LinuxWindow as Window + from ._pywinctl_linux import checkPermissions as checkPermissions + from ._pywinctl_linux import getActiveWindow as getActiveWindow + from ._pywinctl_linux import getActiveWindowTitle as getActiveWindowTitle + from ._pywinctl_linux import getAllAppsNames as getAllAppsNames + from ._pywinctl_linux import getAllAppsWindowsTitles as getAllAppsWindowsTitles + from ._pywinctl_linux import getAllTitles as getAllTitles + from ._pywinctl_linux import getAllWindows as getAllWindows + from ._pywinctl_linux import getAllWindowsDict as getAllWindowsDict + from ._pywinctl_linux import getAppsWithName as getAppsWithName + from ._pywinctl_linux import getTopWindowAt as getTopWindowAt + from ._pywinctl_linux import getWindowsAt as getWindowsAt + from ._pywinctl_linux import getWindowsWithTitle as getWindowsWithTitle else: - raise NotImplementedError('PyWinCtl currently does not support this platform. If you think you can help, please contribute! https://github.com/Kalmat/PyWinCtl') + raise NotImplementedError( + "PyWinCtl currently does not support this platform. " + + "If you think you can help, please contribute! https://github.com/Kalmat/PyWinCtl" + ) +Window = Window diff --git a/src/pywinctl/_pywinctl_linux.py b/src/pywinctl/_pywinctl_linux.py index b34fbef..cc4f385 100644 --- a/src/pywinctl/_pywinctl_linux.py +++ b/src/pywinctl/_pywinctl_linux.py @@ -1,9 +1,10 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- from __future__ import annotations import sys -assert sys.platform == "linux" + +if sys.platform != "linux": + raise OSError(f"Cannot import {__name__} on {sys.platform}") import json import os @@ -11,22 +12,21 @@ import re import subprocess import time -from typing import cast, Optional, Union, List, Tuple +from typing import cast import Xlib.display import Xlib.error +import Xlib.ext import Xlib.protocol import Xlib.X import Xlib.Xatom import Xlib.Xutil -import Xlib.ext -from Xlib.xobject.drawable import Window as XWindow - -from ._main import BaseWindow, Re, _WatchDog, _findMonitorName, _WINDATA, _WINDICT -from ewmhlib import EwmhWindow, EwmhRoot, defaultEwmhRoot, Props +from ewmhlib import EwmhRoot, EwmhWindow, Props, defaultEwmhRoot from ewmhlib._ewmhlib import _xlibGetAllWindows +from pywinbox import Point, Rect, Size, pointInBox +from Xlib.xobject.drawable import Window as XWindow -from pywinbox import Size, Point, Rect, pointInBox +from ._main import _WINDATA, _WINDICT, BaseWindow, Re, _findMonitorName, _WatchDog # WARNING: Changes are not immediately applied, specially for hide/show (unmap/map) # You may set wait to True in case you need to effectively know if/when change has been applied. @@ -46,7 +46,7 @@ def checkPermissions(activate: bool = False) -> bool: return True -def getActiveWindow() -> Optional[LinuxWindow]: +def getActiveWindow() -> LinuxWindow | None: """ Get the currently active (focused) Window in default root @@ -70,7 +70,7 @@ def getActiveWindow() -> Optional[LinuxWindow]: # https://stackoverflow.com/questions/48797323/retrieving-active-window-from-mutter-on-gnome-wayland-session # https://discourse.gnome.org/t/get-window-id-of-a-window-object-window-get-xwindow-doesnt-exist/10956/3 # https://www.reddit.com/r/gnome/comments/d8x27b/is_there_a_program_that_can_show_keypresses_on/ - win_id: Union[str, int] = 0 + win_id: str | int = 0 if os.environ.get('XDG_SESSION_TYPE', '').lower() == "wayland": # IN SWAY: swaymsg -t get_tree | jq '.. | select(.type?) | select(.focused==true).pid' # pynput / mouse --> Not working (no global events allowed, only application events) @@ -120,7 +120,7 @@ def getAllWindows(): return __remove_bad_windows(windows) -def getAllTitles() -> List[str]: +def getAllTitles() -> list[str]: """ Get the list of titles of all visible windows @@ -129,7 +129,7 @@ def getAllTitles() -> List[str]: return [window.title for window in getAllWindows()] -def getWindowsWithTitle(title: Union[str, re.Pattern[str]], app: Optional[Tuple[str, ...]] = (), condition: int = Re.IS, flags: int = 0): +def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | None = (), condition: int = Re.IS, flags: int = 0): """ Get the list of window objects whose title match the given string with condition and flags. Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS) @@ -154,7 +154,7 @@ def getWindowsWithTitle(title: Union[str, re.Pattern[str]], app: Optional[Tuple[ :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of Window objects """ - matches: List[LinuxWindow] = [] + matches: list[LinuxWindow] = [] if title and condition in Re._cond_dic: lower = False if condition in (Re.MATCH, Re.NOTMATCH): @@ -175,7 +175,7 @@ def getWindowsWithTitle(title: Union[str, re.Pattern[str]], app: Optional[Tuple[ return matches -def getAllAppsNames() -> List[str]: +def getAllAppsNames() -> list[str]: """ Get the list of names of all visible apps @@ -184,7 +184,7 @@ def getAllAppsNames() -> List[str]: return list(getAllAppsWindowsTitles()) -def getAppsWithName(name: Union[str, re.Pattern[str]], condition: int = Re.IS, flags: int = 0): +def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0): """ Get the list of app names which match the given string using the given condition and flags. Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS) @@ -208,7 +208,7 @@ def getAppsWithName(name: Union[str, re.Pattern[str]], condition: int = Re.IS, f :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of app names """ - matches: List[str] = [] + matches: list[str] = [] if name and condition in Re._cond_dic: lower = False if condition in (Re.MATCH, Re.NOTMATCH): @@ -254,7 +254,7 @@ def getAllAppsWindowsTitles(): :return: python dictionary """ - result: dict[str, List[str]] = {} + result: dict[str, list[str]] = {} for win in getAllWindows(): appName = win.getAppName() if appName in result.keys(): @@ -332,7 +332,7 @@ def getTopWindowAt(x: int, y: int): :param y: Y screen coordinate of the window :return: Window object or None """ - windows: List[LinuxWindow] = getAllWindows() + windows: list[LinuxWindow] = getAllWindows() for window in reversed(windows): if pointInBox(x, y, window.box): return window @@ -340,20 +340,20 @@ def getTopWindowAt(x: int, y: int): return None -def _WgetAllWindows() -> Tuple[List[dict[str, Union[str, bool]]], dict[str, Union[str, bool]]]: +def _WgetAllWindows() -> tuple[list[dict[str, str | bool]], dict[str, str | bool]]: # POSSIBLE REFERENCE: https://www.roojs.org/seed/gir-1.2-gtk-3.0/seed/Meta.Window.html # Built-in / official apps (e.g. Terminal or gedit) do not fulfill proper get_description() to get the Xid - windowsList: List[dict[str, Union[str, bool]]] = [{}] - activeWindow: dict[str, Union[str, bool]] = {} + windowsList: list[dict[str, str | bool]] = [{}] + activeWindow: dict[str, str | bool] = {} cmd = ('gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell ' '--method org.gnome.Shell.Eval "global.get_window_actors()' '.map(a=>a.meta_window)' '.map(w=>({class: w.get_wm_class(), title: w.get_title(), active: w.has_focus(), id: w.get_description(), id2: w.get_id(), id3: w.get_pid()}))"') ret = subprocess.check_output(cmd, shell=True, timeout=1).decode("utf-8").replace("\n", "") if ret and ret.startswith("(true, "): - windows: List[str] = (str(ret[8:-2]).replace("[", "").replace("]", "").replace("},{", "}|&|{").split("|&|")) + windows: list[str] = (str(ret[8:-2]).replace("[", "").replace("]", "").replace("},{", "}|&|{").split("|&|")) for window in windows: - output: dict[str, Union[str, bool]] = json.loads(window) + output: dict[str, str | bool] = json.loads(window) if str(output.get("id", "")).startswith("0x"): windowsList.append(output) if output.get("active", False): @@ -361,13 +361,14 @@ def _WgetAllWindows() -> Tuple[List[dict[str, Union[str, bool]]], dict[str, Unio return windowsList, activeWindow -def __remove_bad_windows(windows: Optional[Union[List[str], List[int]]]) -> List[LinuxWindow]: +def __remove_bad_windows(windows: list[str] | list[int] | None) -> list[LinuxWindow]: outList = [] if windows is not None: for window in windows: try: # Thanks to Seraphli (https://github.com/Seraphli) for pointing out this issue! - if window: outList.append(LinuxWindow(window)) + if window: + outList.append(LinuxWindow(window)) except: pass return outList @@ -375,7 +376,7 @@ def __remove_bad_windows(windows: Optional[Union[List[str], List[int]]]) -> List class LinuxWindow(BaseWindow): - def __init__(self, hWnd: Union[XWindow, int, str]): + def __init__(self, hWnd: XWindow | int | str) -> None: super().__init__(hWnd) if isinstance(hWnd, XWindow): @@ -392,9 +393,9 @@ def __init__(self, hWnd: Union[XWindow, int, str]): self._currDesktop = os.environ.get('XDG_CURRENT_DESKTOP', "").lower() self._currSessionType = os.environ.get('XDG_SESSION_TYPE', "").lower() - self._motifHints: List[int] = [] + self._motifHints: list[int] = [] - def getExtraFrameSize(self, includeBorder: bool = True) -> Tuple[int, int, int, int]: + def getExtraFrameSize(self, includeBorder: bool = True) -> tuple[int, int, int, int]: """ Get the extra space, in pixels, around the window, including or not the border. Notice not all applications/windows will use this property values @@ -402,13 +403,13 @@ def getExtraFrameSize(self, includeBorder: bool = True) -> Tuple[int, int, int, :param includeBorder: set to ''False'' to avoid including borders :return: additional frame size in pixels, as a tuple of int (left, top, right, bottom) """ - ret: Tuple[int, int, int, int] = (0, 0, 0, 0) + ret: tuple[int, int, int, int] = (0, 0, 0, 0) borderWidth = 0 if includeBorder: geom = self._xWin.get_geometry() borderWidth = geom.border_width if "gnome" in self._currDesktop: - _gtk_extents: List[int] = self._win._getGtkFrameExtents() + _gtk_extents: list[int] = self._win._getGtkFrameExtents() if _gtk_extents and len(_gtk_extents) >= 4: ret = (_gtk_extents[0] + borderWidth, _gtk_extents[2] + borderWidth, _gtk_extents[1] + borderWidth, _gtk_extents[3] + borderWidth) @@ -440,7 +441,7 @@ def getClientFrame(self) -> Rect: ret = Rect(x, y, x + w, y + h) return ret - def __repr__(self): + def __repr__(self) -> str: return '%s(hWnd=%s)' % (self.__class__.__name__, self._hWnd) def __eq__(self, other: object) -> bool: @@ -720,7 +721,7 @@ def acceptInput(self, setTo: bool): self._win.changeWmState(Props.StateAction.REMOVE, Props.State.BELOW) - onebyte = int(0xFF) + onebyte = 0xFF fourbytes = onebyte | (onebyte << 8) | (onebyte << 16) | (onebyte << 24) self._win.changeProperty("_NET_WM_WINDOW_OPACITY", [fourbytes], Xlib.Xatom.CARDINAL) @@ -735,7 +736,7 @@ def acceptInput(self, setTo: bool): self._win.changeWmState(Props.StateAction.ADD, Props.State.BELOW) - onebyte = int(0xFA) # Calculate as 0xff * target_opacity + onebyte = 0xFA # Calculate as 0xff * target_opacity fourbytes = onebyte | (onebyte << 8) | (onebyte << 16) | (onebyte << 24) self._win.changeProperty("_NET_WM_WINDOW_OPACITY", [fourbytes], Xlib.Xatom.CARDINAL) @@ -780,13 +781,13 @@ def setParent(self, parent: int) -> bool: self._xWin.reparent(parent, 0, 0) return bool(self.isChild(parent)) - def getChildren(self) -> List[int]: + def getChildren(self) -> list[int]: """ Get the children handles of current window :return: list of handles """ - return cast(List[int], self._xWin.query_tree().children) + return cast("list[int]", self._xWin.query_tree().children) def getHandle(self) -> int: """ @@ -796,7 +797,7 @@ def getHandle(self) -> int: """ return self._hWnd - def getPID(self) -> Optional[int]: + def getPID(self) -> int | None: """ Get the current application PID the window belongs to @@ -826,7 +827,7 @@ def isChild(self, parent: int): return bool(parent == self.getParent()) isChildOf = isChild # isChildOf is an alias of isParent method - def getDisplay(self) -> List[str]: + def getDisplay(self) -> list[str]: """ Get display names in which current window space is mostly visible @@ -878,7 +879,7 @@ def title(self) -> str: :return: title as a string """ - name: Union[str, bytes] = self._win.getName() + name: str | bytes = self._win.getName() if isinstance(name, bytes): name = name.decode() return name @@ -893,7 +894,7 @@ def visible(self) -> bool: state: int = self._xWin.get_attributes().map_state return bool(state == Xlib.X.IsViewable) - isVisible: bool = cast(bool, visible) # isVisible is an alias for the visible property. + isVisible = visible # isVisible is an alias for the visible property. @property def isAlive(self) -> bool: @@ -903,7 +904,7 @@ def isAlive(self) -> bool: :return: ''True'' if window exists """ try: - state: int = self._xWin.get_attributes().map_state + _state: int = self._xWin.get_attributes().map_state except Xlib.error.BadWindow: return False else: diff --git a/src/pywinctl/_pywinctl_macos.py b/src/pywinctl/_pywinctl_macos.py index 83548e4..9801454 100644 --- a/src/pywinctl/_pywinctl_macos.py +++ b/src/pywinctl/_pywinctl_macos.py @@ -1,11 +1,12 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- # Incomplete type stubs for pyobjc # mypy: disable_error_code = no-any-return from __future__ import annotations import sys -assert sys.platform == "darwin" + +if sys.platform != "darwin": + raise OSError(f"Cannot import {__name__} on {sys.platform}") import ast import difflib @@ -15,7 +16,8 @@ import threading import time from collections.abc import Iterable -from typing import Any, cast, Sequence, Dict, Optional, Union, List, Tuple +from typing import Any, cast, ClassVar +from collections.abc import Sequence from typing_extensions import TypeAlias, TypedDict, Literal import AppKit @@ -26,7 +28,7 @@ Incomplete: TypeAlias = Any -Attribute: TypeAlias = Sequence['Tuple[str, str, bool, str]'] +Attribute: TypeAlias = Sequence['tuple[str, str, bool, str]'] WAIT_ATTEMPTS = 10 WAIT_DELAY = 0.025 # Will be progressively increased on every retry @@ -65,7 +67,7 @@ def checkPermissions(activate: bool = False) -> bool: return ret == "true" -def getActiveWindow() -> Optional[MacOSWindow]: +def getActiveWindow() -> MacOSWindow | None: """ Get the currently active (focused) Window @@ -117,14 +119,14 @@ def getActiveWindowTitle() -> str: return "" -def getAllWindows() -> List[MacOSWindow]: +def getAllWindows() -> list[MacOSWindow]: """ Get the list of Window objects for all visible windows :return: list of Window objects """ # TODO: Find a way to return windows as per the stacking order (not sure if it is even possible!) - windows: List[MacOSWindow] = [] + windows: list[MacOSWindow] = [] activeApps = _getAllApps() titleList = _getWindowTitles() for item in titleList: @@ -140,7 +142,7 @@ def getAllWindows() -> List[MacOSWindow]: return windows -def getAllTitles() -> List[str]: +def getAllTitles() -> list[str]: """ Get the list of titles of all visible windows @@ -157,7 +159,7 @@ def getAllTitles() -> List[str]: .replace('missing value', '"missing value"') \ .replace("{", "[").replace("}", "]") res = ast.literal_eval(ret) - matches: List[str] = [] + matches: list[str] = [] if len(res) > 0: for item in res[0]: for title in item: @@ -165,7 +167,7 @@ def getAllTitles() -> List[str]: return matches -def getWindowsWithTitle(title: Union[str, re.Pattern[str]], condition: int = Re.IS, flags: int = 0) -> List[MacOSWindow]: +def getWindowsWithTitle(title: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0) -> list[MacOSWindow]: """ Get the list of window objects whose title match the given string with condition and flags. Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS) @@ -205,7 +207,7 @@ def getWindowsWithTitle(title: Union[str, re.Pattern[str]], condition: int = Re. title = title.pattern title = title.lower() - matches: List[MacOSWindow] = [] + matches: list[MacOSWindow] = [] activeApps = _getAllApps() titleList = _getWindowTitles() for item in titleList: @@ -219,7 +221,7 @@ def getWindowsWithTitle(title: Union[str, re.Pattern[str]], condition: int = Re. return matches -def getAllAppsNames() -> List[str]: +def getAllAppsNames() -> list[str]: """ Get the list of names of all visible apps @@ -239,7 +241,7 @@ def getAllAppsNames() -> List[str]: return res or [] -def getAppsWithName(name: Union[str, re.Pattern[str]], condition: int = Re.IS, flags: int = 0): +def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0): """ Get the list of app names which match the given string using the given condition and flags. Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS) @@ -263,7 +265,7 @@ def getAppsWithName(name: Union[str, re.Pattern[str]], condition: int = Re.IS, f :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of app names """ - matches: List[str] = [] + matches: list[str] = [] if name and condition in Re._cond_dic: lower = False if condition in (Re.MATCH, Re.NOTMATCH): @@ -303,8 +305,8 @@ def getAllAppsWindowsTitles(): ret = subprocess.check_output(cmd, shell=True).decode(encoding="utf-8") \ .replace('missing value', '"missing value"') \ .replace("\n", "").replace("{", "[").replace("}", "]") - res: Tuple[List[str], List[List[str]]] = ast.literal_eval(ret) - result: dict[str, List[str]] = {} + res: tuple[list[str], list[list[str]]] = ast.literal_eval(ret) + result: dict[str, list[str]] = {} if res and len(res) > 0: for i, item in enumerate(res[0]): result[item] = res[1][i] @@ -376,7 +378,7 @@ def getAllWindowsDict(tryToFilter: bool = False) -> dict[str, _WINDICT]: return result -def getWindowsAt(x: int, y: int, allWindows: Optional[List[MacOSWindow]] = None) -> List[MacOSWindow]: +def getWindowsAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None) -> list[MacOSWindow]: """ Get the list of Window objects whose windows contain the point ``(x, y)`` on screen @@ -393,7 +395,7 @@ def getWindowsAt(x: int, y: int, allWindows: Optional[List[MacOSWindow]] = None) if pointInBox(x, y, box)] -def getTopWindowAt(x: int, y: int, allWindows: Optional[List[MacOSWindow]] = None) -> Optional[MacOSWindow]: +def getTopWindowAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None) -> MacOSWindow | None: """ Get *a* Window object at the point ``(x, y)`` on screen. Which window is not guaranteed. See https://github.com/Kalmat/PyWinCtl/issues/20#issuecomment-1193348238 @@ -411,7 +413,7 @@ def getTopWindowAt(x: int, y: int, allWindows: Optional[List[MacOSWindow]] = Non def _getAllApps(userOnly: bool = True): - matches: List[AppKit.NSRunningApplication] = [] + matches: list[AppKit.NSRunningApplication] = [] for app in AppKit.NSWorkspace.sharedWorkspace().runningApplications(): if not userOnly or (userOnly and app.activationPolicy() == Quartz.NSApplicationActivationPolicyRegular): matches.append(app) @@ -439,7 +441,7 @@ def _getAppWindowsTitles(app: AppKit.NSRunningApplication): return res or [] -def _getWindowTitles() -> List[List[str]]: +def _getWindowTitles() -> list[list[str]]: # https://gist.github.com/qur2/5729056 - qur2 cmd = """osascript -s 's' -e 'tell application "System Events" set winNames to {} @@ -452,7 +454,7 @@ def _getWindowTitles() -> List[List[str]]: .replace('missing value', '"missing value"') \ .replace("{", "[").replace("}", "]") res = ast.literal_eval(ret) - result: List[List[str]] = [] + result: list[list[str]] = [] if len(res) > 0: for i, pID in enumerate(res[0]): try: @@ -486,7 +488,7 @@ class _SubMenuStructure(TypedDict, total=False): class MacOSWindow(BaseWindow): - def __init__(self, app: AppKit.NSRunningApplication, title: str): + def __init__(self, app: AppKit.NSRunningApplication, title: str) -> None: super().__init__((app.localizedName(), title)) self._app = app @@ -502,9 +504,9 @@ def __init__(self, app: AppKit.NSRunningApplication, title: str): ver = float(v[0]+"."+v[1]) # On Yosemite and below we need to use Zoom instead of FullScreen to maximize windows self._use_zoom = (ver <= 10.10) - self._tt: Optional[_SendTop] = None + self._tt: _SendTop | None = None self._kill_tt = threading.Event() - self._tb: Optional[_SendBottom] = None + self._tb: _SendBottom | None = None self._kill_tb = threading.Event() self.menu = self._Menu(self) self.watchdog = _WatchDog(self) @@ -525,7 +527,7 @@ def getProcName(self, appPID): ret, err = proc.communicate(cmd) return str(ret.replace("\n", "")) - def getExtraFrameSize(self, includeBorder: bool = True) -> Tuple[int, int, int, int]: + def getExtraFrameSize(self, includeBorder: bool = True) -> tuple[int, int, int, int]: """ Get the invisible space, in pixels, around the window, including or not the visible resize border @@ -553,10 +555,11 @@ def getClientFrame(self): class WindowDelegate(AppKit.NSObject): # type: ignore[no-redef] """super-iby: Helps run window operations on the main thread.""" - results: Dict[bytes, Any] = {} # Store results here. Not ideal, but may be better than using a global. + results: ClassVar[dict[bytes, Any]] = {} + """Store results here. Not ideal, but may be better than using a global.""" @staticmethod - def run_on_main_thread(selector: bytes, obj: Optional[Any] = None, wait: Optional[bool] = True) -> Any: + def run_on_main_thread(selector: bytes, obj: Any | None = None, wait: bool | None = True) -> Any: """Runs a method of this object on the main thread.""" WindowDelegate.alloc().performSelectorOnMainThread_withObject_waitUntilDone_(selector, obj, wait) return WindowDelegate.results.get(selector) @@ -585,10 +588,10 @@ def getTitleBarHeightAndBorderWidth(self) -> None: res = Rect(int(self.left + borderWidth), int(self.top + titleHeight), int(self.right - borderWidth), int(self.bottom - borderWidth)) return res - def __repr__(self): + def __repr__(self) -> str: return '%s(hWnd=%s)' % (self.__class__.__name__, self._app) - def __eq__(self, other: object): + def __eq__(self, other: object) -> bool: return isinstance(other, MacOSWindow) and self._app == other._app def close(self, force: bool = False) -> bool: @@ -1047,7 +1050,7 @@ def getAppName(self) -> str: """ return self._appName - def getParent(self) -> Tuple[str, str]: + def getParent(self) -> tuple[str, str]: """ Get the handle of the current window parent. It can be another window or an application @@ -1085,7 +1088,7 @@ def getParent(self) -> Tuple[str, str]: result = self._appName, parent return result - def setParent(self, parent: Tuple[str, str]): + def setParent(self, parent: tuple[str, str]): """ Current window will become child of given parent WARNIG: Not implemented in AppleScript (not possible in macOS for foreign (other apps') windows) @@ -1101,7 +1104,7 @@ def getChildren(self): :return: list of handles as tuples (appName, windowTitle) """ - result: List[Tuple[str, str]] = [] + result: list[tuple[str, str]] = [] if not self._winTitle: return result @@ -1127,7 +1130,7 @@ def getChildren(self): result.append((self._appName, res)) return result - def getHandle(self) -> Tuple[str, str]: + def getHandle(self) -> tuple[str, str]: """ Get the current window handle @@ -1138,7 +1141,7 @@ def getHandle(self) -> Tuple[str, str]: return "", "" return self._appName, title - def getPID(self) -> Optional[int]: + def getPID(self) -> int | None: """ Get the current application PID the window belongs to @@ -1157,7 +1160,7 @@ def getPID(self) -> Optional[int]: return int(ret) return None - def isParent(self, child: Tuple[str, str]) -> bool: + def isParent(self, child: tuple[str, str]) -> bool: """ Check if current window is parent of given window (handle) @@ -1168,7 +1171,7 @@ def isParent(self, child: Tuple[str, str]) -> bool: return child in children isParentOf = isParent # isParentOf is an alias of isParent method - def isChild(self, parent: Tuple[str, str]) -> bool: + def isChild(self, parent: tuple[str, str]) -> bool: """ Check if current window is child of given window/app (handle) @@ -1179,7 +1182,7 @@ def isChild(self, parent: Tuple[str, str]) -> bool: return currParent == parent isChildOf = isChild # isParentOf is an alias of isParent method - def getDisplay(self) -> List[str]: + def getDisplay(self) -> list[str]: """ Get display names in which current window space is mostly visible @@ -1320,7 +1323,7 @@ def visible(self) -> bool: """ return bool(self._winTitle and self._winTitle in _getAppWindowsTitles(self._app)) - isVisible: bool = cast(bool, visible) # isVisible is an alias for the visible property. + isVisible = visible # isVisible is an alias for the visible property. @property def isAlive(self) -> bool: @@ -1361,11 +1364,11 @@ def isAlive(self) -> bool: class _Menu: - def __init__(self, parent: MacOSWindow): + def __init__(self, parent: MacOSWindow) -> None: self._parent = parent self._menuStructure: dict[str, _SubMenuStructure] = {} - self.menuList: List[str] = [] - self.itemList: List[str] = [] + self.menuList: list[str] = [] + self.itemList: list[str] = [] self.SEP = "|&|" def getMenu(self, addItemInfo: bool = False) -> dict[str, _SubMenuStructure]: @@ -1409,12 +1412,12 @@ def getMenu(self, addItemInfo: bool = False) -> dict[str, _SubMenuStructure]: self.menuList = [] self.itemList = [] - nameList: List[Incomplete] = [] + nameList: list[Incomplete] = [] # Nested recursive types. Dept based on size of nameList. # Very complex to type. - sizeList: List[Sequence[Any]] = [] - posList: List[Sequence[Any]] = [] - attrList: List[Sequence[Any]] = [] + sizeList: list[Sequence[Any]] = [] + posList: list[Sequence[Any]] = [] + attrList: list[Sequence[Any]] = [] def findit(): @@ -1487,13 +1490,13 @@ def fillit(): def subfillit( subNameList: Iterable[str], - subSizeList: Sequence[Union[Tuple[int, int], Literal["missing value"]]], - subPosList: Sequence[Union[Tuple[int, int], Literal["missing value"]]], + subSizeList: Sequence[tuple[int, int] | Literal["missing value"]], + subPosList: Sequence[tuple[int, int] | Literal["missing value"]], subAttrList: Sequence[Attribute], section: str = "", level: int = 0, mainlevel: int = 0, - path: Optional[Sequence[int]] = None, + path: Sequence[int] | None = None, parent: int = 0, ): path = list(path or []) @@ -1506,7 +1509,7 @@ def subfillit( for i, name in enumerate(subNameList): pos = subPosList[i] if len(subPosList) > i else "missing value" size = subSizeList[i] if len(subSizeList) > i else "missing value" - attr: Union[str, Attribute] = subAttrList[i] if (addItemInfo and len(subAttrList) > 0) else [] + attr: str | Attribute = subAttrList[i] if (addItemInfo and len(subAttrList) > 0) else [] if not name: continue elif name == "missing value": @@ -1542,22 +1545,23 @@ def subfillit( option[name]["entries"] = {} subfillit(submenu, subSize, subPos, subAttr, section + self.SEP + name + self.SEP + "entries", - level=level + 1, mainlevel=mainlevel, path=[level+1, mainlevel, 0]+subPath, + level=level + 1, mainlevel=mainlevel, path=[level + 1, mainlevel, 0, *subPath], parent=hSubMenu) else: option[name]["hSubMenu"] = 0 - for i, item in enumerate(cast("List[str]", nameList[0])): + for i, item in enumerate(cast("list[str]", nameList[0])): hSubMenu = self._getNewHSubMenu(item) self._menuStructure[item] = {"hSubMenu": hSubMenu, "wID": self._getNewWid(item), "entries": {}} subfillit(nameList[1][i][0], sizeList[1][i][0], posList[1][i][0], attrList[1][i][0] if addItemInfo else [], item + self.SEP + "entries", level=1, mainlevel=i, path=[1, i, 0], parent=hSubMenu) - if findit(): fillit() + if findit(): + fillit() return self._menuStructure - def clickMenuItem(self, itemPath: Optional[Sequence[str]] = None, wID: int = 0) -> bool: + def clickMenuItem(self, itemPath: Sequence[str] | None = None, wID: int = 0) -> bool: """ Simulates a click on a menu item @@ -1779,13 +1783,13 @@ def getMenuItemRect(self, hSubMenu: int, wID: int) -> Rect: return Rect(x, y, x + w, y + h) - def _isListEmpty(self, inList: List[Any]): + def _isListEmpty(self, inList: list[Any]): # https://stackoverflow.com/questions/1593564/python-how-to-check-if-a-nested-list-is-essentially-empty/51582274 if isinstance(inList, list): return all(map(self._isListEmpty, inList)) return False - def _parseAttr(self, attr: Union[str, Attribute]): + def _parseAttr(self, attr: str | Attribute): itemInfo: dict[str, _ItemInfoValue] = {} if isinstance(attr, str): @@ -1842,7 +1846,7 @@ def _getMenuItemWid(self, itemPath: str): wID = option.get(itemPath[-1], {}).get("wID", 0) return wID - def _getaccesskey(self, item_info: Union[Dict[str, Dict[str, str]], Dict[str, _ItemInfoValue]]): + def _getaccesskey(self, item_info: dict[str, dict[str, str]] | dict[str, _ItemInfoValue]): # https://github.com/babarrett/hammerspoon/blob/master/cheatsheets.lua # https://github.com/pyatom/pyatom/blob/master/atomac/ldtpd/core.py @@ -1912,7 +1916,7 @@ def _getaccesskey(self, item_info: Union[Dict[str, Dict[str, str]], Dict[str, _I class _SendTop(threading.Thread): - def __init__(self, hWnd: MacOSWindow, kill: threading.Event, interval: float = 0.5): + def __init__(self, hWnd: MacOSWindow, kill: threading.Event, interval: float = 0.5) -> None: threading.Thread.__init__(self) self._hWnd = hWnd self._kill = kill @@ -1927,7 +1931,7 @@ def run(self): class _SendBottom(threading.Thread): - def __init__(self, hWnd: MacOSWindow, kill: threading.Event, interval: float = 0.5): + def __init__(self, hWnd: MacOSWindow, kill: threading.Event, interval: float = 0.5) -> None: threading.Thread.__init__(self) self._hWnd = hWnd self._app = hWnd._app @@ -1935,7 +1939,7 @@ def __init__(self, hWnd: MacOSWindow, kill: threading.Event, interval: float = 0 self._kill = kill self._interval = interval _apps = _getAllApps() - self._apps: List[AppKit.NSRunningApplication] = [] + self._apps: list[AppKit.NSRunningApplication] = [] for app in _apps: if app.processIdentifier() != self._appPID: self._apps.append(app) diff --git a/src/pywinctl/_pywinctl_win.py b/src/pywinctl/_pywinctl_win.py index 0c4280d..358eb13 100644 --- a/src/pywinctl/_pywinctl_win.py +++ b/src/pywinctl/_pywinctl_win.py @@ -1,9 +1,10 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- from __future__ import annotations import sys -assert sys.platform == "win32" + +if sys.platform != "win32": + raise OSError(f"Cannot import {__name__} on {sys.platform}") import ctypes import re @@ -11,7 +12,7 @@ import time from collections.abc import Sequence from ctypes import wintypes -from typing import cast, Any, TYPE_CHECKING, Tuple, Optional, Union, List +from typing import cast, Any, TYPE_CHECKING from typing_extensions import NotRequired, TypedDict if TYPE_CHECKING: @@ -45,7 +46,7 @@ def checkPermissions(activate: bool = False) -> bool: return True -def getActiveWindow() -> Optional[Win32Window]: +def getActiveWindow() -> Win32Window | None: """ Get the currently active (focused) Window @@ -71,7 +72,7 @@ def getActiveWindowTitle() -> str: return "" -def getAllWindows() -> List[Win32Window]: +def getAllWindows() -> list[Win32Window]: """ Get the list of Window objects for all visible windows @@ -82,7 +83,7 @@ def getAllWindows() -> List[Win32Window]: return [window for window in __remove_bad_windows(_findWindowHandles())] -def __remove_bad_windows(windows: Optional[List[int]]): +def __remove_bad_windows(windows: list[int] | None): """ :param windows: win32 Windows :return: A generator of Win32Window that filters out BadWindows @@ -97,7 +98,7 @@ def __remove_bad_windows(windows: Optional[List[int]]): return outList -def getAllTitles() -> List[str]: +def getAllTitles() -> list[str]: """ Get the list of titles of all visible windows @@ -106,7 +107,7 @@ def getAllTitles() -> List[str]: return [window.title for window in getAllWindows()] -def getWindowsWithTitle(title: Union[str, re.Pattern[str]], app: Optional[Tuple[str, ...]] = (), condition: int = Re.IS, flags: int = 0) -> List[Win32Window]: +def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | None = (), condition: int = Re.IS, flags: int = 0) -> list[Win32Window]: """ Get the list of window objects whose title match the given string with condition and flags. Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS) @@ -131,7 +132,7 @@ def getWindowsWithTitle(title: Union[str, re.Pattern[str]], app: Optional[Tuple[ :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of Window objects """ - matches: List[Win32Window] = [] + matches: list[Win32Window] = [] if title and condition in Re._cond_dic: lower = False if condition in (Re.MATCH, Re.NOTMATCH): @@ -152,7 +153,7 @@ def getWindowsWithTitle(title: Union[str, re.Pattern[str]], app: Optional[Tuple[ return matches -def getAllAppsNames() -> List[str]: +def getAllAppsNames() -> list[str]: """ Get the list of names of all visible apps @@ -161,7 +162,7 @@ def getAllAppsNames() -> List[str]: return list(getAllAppsWindowsTitles().keys()) -def getAppsWithName(name: Union[str, re.Pattern[str]], condition: int = Re.IS, flags: int = 0) -> List[str]: +def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0) -> list[str]: """ Get the list of app names which match the given string using the given condition and flags. Use ''condition'' to delimit the search. Allowed values are stored in pywinctl.Re sub-class (e.g. pywinctl.Re.CONTAINS) @@ -185,7 +186,7 @@ def getAppsWithName(name: Union[str, re.Pattern[str]], condition: int = Re.IS, f :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of app names """ - matches: List[str] = [] + matches: list[str] = [] if name and condition in Re._cond_dic: lower = False if condition in (Re.MATCH, Re.NOTMATCH): @@ -204,7 +205,7 @@ def getAppsWithName(name: Union[str, re.Pattern[str]], condition: int = Re.IS, f return matches -def getAllAppsWindowsTitles() -> dict[str, List[str]]: +def getAllAppsWindowsTitles() -> dict[str, list[str]]: """ Get all visible apps names and their open windows titles @@ -216,7 +217,7 @@ def getAllAppsWindowsTitles() -> dict[str, List[str]]: :return: python dictionary """ process_list = _getAllApps(tryToFilter=True) - result: dict[str, List[str]] = {} + result: dict[str, list[str]] = {} for win in getAllWindows(): pID = win32process.GetWindowThreadProcessId(win.getHandle()) for item in process_list: @@ -283,7 +284,7 @@ def getAllWindowsDict(tryToFilter: bool = False) -> dict[str, _WINDICT]: return result -def getWindowsAt(x: int, y: int) -> List[Win32Window]: +def getWindowsAt(x: int, y: int) -> list[Win32Window]: """ Get the list of Window objects whose windows contain the point ``(x, y)`` on screen @@ -298,7 +299,7 @@ def getWindowsAt(x: int, y: int) -> List[Win32Window]: if pointInBox(x, y, box)] -def getTopWindowAt(x: int, y: int) -> Optional[Win32Window]: +def getTopWindowAt(x: int, y: int) -> Win32Window | None: """ Get the Window object at the top of the stack at the point ``(x, y)`` on screen @@ -315,9 +316,9 @@ def getTopWindowAt(x: int, y: int) -> Optional[Win32Window]: return Win32Window(hwnd) if hwnd else None -def _findWindowHandles(parent: Optional[int] = None, window_class: Optional[str] = None, title: Optional[str] = None, onlyVisible: bool = True) -> List[int]: +def _findWindowHandles(parent: int | None = None, window_class: str | None = None, title: str | None = None, onlyVisible: bool = True) -> list[int]: - handle_list: List[int] = [] + handle_list: list[int] = [] def findit(hwnd: int, ctx: Any) -> bool: @@ -330,25 +331,25 @@ def findit(hwnd: int, ctx: Any) -> bool: return True if not parent: - parent = win32gui.GetDesktopWindow() # type: ignore[no-untyped-call] # pyright: ignore[reportUnknownMemberType] + parent = win32gui.GetDesktopWindow() win32gui.EnumChildWindows(parent, findit, None) return handle_list -def _findMainWindowHandles() -> List[Tuple[int, int]]: +def _findMainWindowHandles() -> list[tuple[int, int]]: # Filter windows: https://stackoverflow.com/questions/64586371/filtering-background-processes-pywin32 class TITLEBARINFO(ctypes.Structure): if TYPE_CHECKING: cbSize: int rcTitleBar: wintypes.RECT - rgstate: List[int] + rgstate: list[int] def __init__( self, cbSize: int = ..., rcTitleBar: wintypes.RECT = ..., - rgstate: List[int] = ... - ): ... + rgstate: list[int] = ... + ) -> None: ... _fields_ = [ ("cbSize", wintypes.DWORD), @@ -378,12 +379,12 @@ def winEnumHandler(hwnd: int, ctx: Any): if not (title_info.rgstate[0] & win32con.STATE_SYSTEM_INVISIBLE): handle_list.append((hwnd, win32process.GetWindowThreadProcessId(hwnd)[1])) - handle_list: List[Tuple[int, int]] = [] + handle_list: list[tuple[int, int]] = [] win32gui.EnumWindows(winEnumHandler, None) return handle_list -def _getAllApps(tryToFilter: bool = False) -> Union[List[Tuple[int, Optional[str]]], List[Tuple[int, str]]]: +def _getAllApps(tryToFilter: bool = False) -> list[tuple[int, str | None]] | list[tuple[int, str]]: # https://stackoverflow.com/questions/550653/cross-platform-way-to-get-pids-by-process-name-in-python WMI = GetObject('winmgmts:') if tryToFilter: @@ -394,10 +395,10 @@ def _getAllApps(tryToFilter: bool = False) -> Union[List[Tuple[int, Optional[str return [(p.Properties_("ProcessID").Value, p.Properties_("Name").Value) for p in WMI.InstancesOf('Win32_Process')] -def _getAllAppsDict(tryToFilter: bool = False) -> dict[str, Union[int, str]]: +def _getAllAppsDict(tryToFilter: bool = False) -> dict[str, int | str]: # https://stackoverflow.com/questions/550653/cross-platform-way-to-get-pids-by-process-name-in-python WMI = GetObject('winmgmts:') - result: dict[str, Union[int, str]] = {} + result: dict[str, int | str] = {} if tryToFilter: mainWindows = [w[1] for w in _findMainWindowHandles()] for p in WMI.InstancesOf('Win32_Process'): @@ -434,7 +435,7 @@ def __init__( cyWindowBorders: int = ..., atomWindowType: int = ..., wCreatorVersion: int = ... - ): ... + ) -> None: ... _fields_ = [ ('cbSize', wintypes.DWORD), @@ -450,7 +451,7 @@ def __init__( ] -def _getWindowInfo(hWnd: Optional[Union[int, str, bytes, bool]]) -> tagWINDOWINFO: +def _getWindowInfo(hWnd: int | str | bytes | bool | None) -> tagWINDOWINFO: # PWINDOWINFO = ctypes.POINTER(tagWINDOWINFO) # LPWINDOWINFO = ctypes.POINTER(tagWINDOWINFO) @@ -478,30 +479,30 @@ def _getWindowInfo(hWnd: Optional[Union[int, str, bytes, bool]]) -> tagWINDOWINF class _SubMenuStructure(TypedDict): hSubMenu: int - wID: Optional[int] + wID: int | None entries: dict[str, _SubMenuStructure] parent: int - rect: Optional[Rect] + rect: Rect | None item_info: NotRequired[_MENUITEMINFO] shortcut: str class Win32Window(BaseWindow): - def __init__(self, hWnd: Union[int, str]): + def __init__(self, hWnd: int | str) -> None: super().__init__(hWnd) self._hWnd = int(hWnd, base=16) if isinstance(hWnd, str) else int(hWnd) self._parent = win32gui.GetParent(self._hWnd) - self._t: Optional[_SendBottom] = None + self._t: _SendBottom | None = None self._kill_t = threading.Event() self.menu = self._Menu(self) self.watchdog = _WatchDog(self) - self._hDpy: Optional[int] = None + self._hDpy: int | None = None self._display: str = "" - def getExtraFrameSize(self, includeBorder: bool = True) -> Tuple[int, int, int, int]: + def getExtraFrameSize(self, includeBorder: bool = True) -> tuple[int, int, int, int]: """ Get the invisible space, in pixels, around the window, including or not the visible resize border (usually 1px) This can be useful to accurately adjust window position and size to the desired visible space @@ -537,7 +538,7 @@ def getClientFrame(self) -> Rect: """ wi = _getWindowInfo(self._hWnd) if wi: - rcClient: Rect = cast(Rect, wi.rcClient) + rcClient: Rect = cast("Rect", wi.rcClient) else: rcClient = self.rect return Rect(rcClient.left, rcClient.top, rcClient.right, rcClient.bottom) @@ -797,9 +798,9 @@ def sendBehind(self, sb: bool = True) -> bool: """ # https://www.codeproject.com/Articles/856020/Draw-Behind-Desktop-Icons-in-Windows-plus if sb: - def getWorkerW() -> List[int]: + def getWorkerW() -> list[int]: - thelist: List[int] = [] + thelist: list[int] = [] def findit(hwnd: int, ctx: Any): p = win32gui.FindWindowEx(hwnd, None, "SHELLDLL_DefView", "") @@ -819,7 +820,8 @@ def findit(hwnd: int, ctx: Any): else: ret = win32gui.SetParent(self._hWnd, self._parent) win32gui.DefWindowProc(self._hWnd, 0x0128, 3 | 0x4, 0) - win32gui.RedrawWindow(self._hWnd, win32gui.GetWindowRect(self._hWnd), 0, 0) # type: ignore[arg-type] # pyright: ignore[reportUnknownMemberType, reportGeneralTypeIssues] # We expect an error here + # We expect an error here, but on non-win32 mypy says unused-ignore + win32gui.RedrawWindow(self._hWnd, win32gui.GetWindowRect(self._hWnd), 0, 0) # type: ignore[arg-type, unused-ignore] # pyright: ignore[reportUnknownMemberType, reportGeneralTypeIssues] return ret != 0 def acceptInput(self, setTo: bool): @@ -869,7 +871,7 @@ def setParent(self, parent: int) -> bool: win32gui.SetParent(self._hWnd, parent) return bool(self.isChild(parent)) - def getChildren(self) -> List[int]: + def getChildren(self) -> list[int]: """ Get the children handles of current window @@ -885,7 +887,7 @@ def getHandle(self) -> int: """ return self._hWnd - def getPID(self) -> Optional[int]: + def getPID(self) -> int | None: """ Get the current application PID the window belongs to @@ -916,7 +918,7 @@ def isChild(self, parent: int) -> bool: return parent == self.getParent() isChildOf = isChild # isChildOf is an alias of isParent method - def getDisplay(self) -> List[str]: + def getDisplay(self) -> list[str]: """ Get display names in which current window space is mostly visible @@ -984,7 +986,7 @@ def visible(self) -> bool: # https://github.com/python/mypy/issues/2563 # https://github.com/python/mypy/issues/11619 # https://github.com/python/mypy/issues/13975 - isVisible: bool = cast(bool, visible) # isVisible is an alias for the visible property. + isVisible = visible # isVisible is an alias for the visible property. @property def isAlive(self) -> bool: @@ -1086,7 +1088,7 @@ def isAlive(self) -> bool: class _Menu: - def __init__(self, parent: Win32Window): + def __init__(self, parent: Win32Window) -> None: self._parent = parent self._hWnd = parent.getHandle() self._hMenu = win32gui.GetMenu(self._hWnd) @@ -1124,7 +1126,7 @@ def getMenu(self, addItemInfo: bool = False) -> dict[str, _SubMenuStructure]: sub-items within the sub-menu (if any) """ - def findit(parent: int, level: str = "", parentRect: Optional[Rect] = None): + def findit(parent: int, level: str = "", parentRect: Rect | None = None): option = self._menuStructure if level: @@ -1132,7 +1134,7 @@ def findit(parent: int, level: str = "", parentRect: Optional[Rect] = None): option = cast("dict[str, _SubMenuStructure]", option[section]) for i in range(win32gui.GetMenuItemCount(parent)): - item_info: Optional[_MENUITEMINFO] = self._getMenuItemInfo(hSubMenu=parent, itemPos=i) + item_info: _MENUITEMINFO | None = self._getMenuItemInfo(hSubMenu=parent, itemPos=i) if not item_info or not item_info.text or item_info.hSubMenu is None: continue text = item_info.text.split("\t") @@ -1149,7 +1151,7 @@ def findit(parent: int, level: str = "", parentRect: Optional[Rect] = None): findit(self._hMenu) return self._menuStructure - def clickMenuItem(self, itemPath: Optional[Sequence[str]] = None, wID: Optional[int] = 0) -> bool: + def clickMenuItem(self, itemPath: Sequence[str] | None = None, wID: int | None = 0) -> bool: """ Simulates a click on a menu item @@ -1180,7 +1182,7 @@ def clickMenuItem(self, itemPath: Optional[Sequence[str]] = None, wID: Optional[ break if option and itemPath[-1] in option: - itemID = cast(int, option[itemPath[-1]]["wID"]) + itemID = cast("int", option[itemPath[-1]]["wID"]) if itemID: win32gui.PostMessage(self._hWnd, win32con.WM_COMMAND, itemID, 0) @@ -1188,7 +1190,7 @@ def clickMenuItem(self, itemPath: Optional[Sequence[str]] = None, wID: Optional[ return found - def getMenuInfo(self, hSubMenu: int = 0) -> Optional[_MENUINFO]: + def getMenuInfo(self, hSubMenu: int = 0) -> _MENUINFO | None: """ Returns the MENUINFO struct of the given sub-menu or main menu if none given @@ -1216,7 +1218,7 @@ def getMenuItemCount(self, hSubMenu: int = 0) -> int: hSubMenu = self._hMenu return win32gui.GetMenuItemCount(hSubMenu) - def getMenuItemInfo(self, hSubMenu: int, wID: int) -> Optional[_MENUITEMINFO]: + def getMenuItemInfo(self, hSubMenu: int, wID: int) -> _MENUITEMINFO | None: """ Returns the MENUITEMINFO struct for the given menu item @@ -1231,7 +1233,7 @@ def getMenuItemInfo(self, hSubMenu: int, wID: int) -> Optional[_MENUITEMINFO]: item_info = win32gui_struct.UnpackMENUITEMINFO(buf) return item_info - def _getMenuItemInfo(self, hSubMenu: int, itemPos: int) -> Optional[_MENUITEMINFO]: + def _getMenuItemInfo(self, hSubMenu: int, itemPos: int) -> _MENUITEMINFO | None: item_info = None if self._hMenu: buf, _extras = win32gui_struct.EmptyMENUITEMINFO() @@ -1247,9 +1249,9 @@ def getMenuItemRect(self, hSubMenu: int, wID: int) -> Rect: :param wID: id of the window within menu struct (as returned by :meth: getMenu()) :return: Rect struct """ - def findit(menu: dict[str, _SubMenuStructure], hSubMenu: int, wID: Optional[int]) -> int: + def findit(menu: dict[str, _SubMenuStructure], hSubMenu: int, wID: int | None) -> int: - menuFound: List[dict[str, _SubMenuStructure]] = [{}] + menuFound: list[dict[str, _SubMenuStructure]] = [{}] def findMenu(inMenu: dict[str, _SubMenuStructure], hSubMenu: int): @@ -1280,7 +1282,7 @@ def findMenu(inMenu: dict[str, _SubMenuStructure], hSubMenu: int): ret = Rect(x, y, r, b) return ret - def _getMenuItemRect(self, hSubMenu: int, itemPos: int, parentRect: Optional[Rect] = None, relative: bool = False) -> Optional[Rect]: + def _getMenuItemRect(self, hSubMenu: int, itemPos: int, parentRect: Rect | None = None, relative: bool = False) -> Rect | None: ret = None if self._hMenu and hSubMenu and 0 <= itemPos < self.getMenuItemCount(hSubMenu=hSubMenu): result, (x, y, r, b) = win32gui.GetMenuItemRect(self._hWnd, hSubMenu, itemPos) @@ -1298,7 +1300,7 @@ def _getMenuItemRect(self, hSubMenu: int, itemPos: int, parentRect: Optional[Rec class _SendBottom(threading.Thread): - def __init__(self, hWnd: int, kill: threading.Event, interval: float = 0.5): + def __init__(self, hWnd: int, kill: threading.Event, interval: float = 0.5) -> None: threading.Thread.__init__(self) self._hWnd = hWnd self._kill = kill diff --git a/tests/test_pywinctl.py b/tests/test_pywinctl.py index 080fa7a..f784d0e 100644 --- a/tests/test_pywinctl.py +++ b/tests/test_pywinctl.py @@ -1,5 +1,4 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- from __future__ import annotations import subprocess @@ -15,7 +14,7 @@ class GetWindowKwargs(TypedDict): condition: int # TODO: Consider making pywinctl.Re an IntEnum -def test_basic(): +def test_basic() -> None: print("PLATFORM:", sys.platform) print() @@ -68,7 +67,7 @@ def test_basic(): basic_test(npw=testWindows[0], wait=True, timelap=0.50) -def basic_test(npw: pywinctl.Window | None, wait: bool, timelap: float): +def basic_test(npw: pywinctl.Window | None, wait: bool, timelap: float) -> None: assert npw is not None def test_moveresize(attr, value): @@ -209,12 +208,12 @@ def activeCB(isActive): print("LOWER WINDOW") lowered = npw.lowerWindow() time.sleep(timelap*3) - # assert lowered, 'Window has not been lowered' + assert lowered, 'Window has not been lowered' print("RAISE WINDOW") raised = npw.raiseWindow() time.sleep(timelap) - # assert raised, 'Window has not been raised' + assert raised, 'Window has not been raised' if sys.platform != "darwin": print("SEND BEHIND") diff --git a/typings/Quartz/__init__.pyi b/typings/Quartz/__init__.pyi index 7a1a07a..66a47e6 100644 --- a/typings/Quartz/__init__.pyi +++ b/typings/Quartz/__init__.pyi @@ -15,4 +15,4 @@ from Quartz.QuartzCore import * from Quartz.QuartzFilters import * from Quartz.QuickLookUI import * -def __getattr__(__name: str) -> Any: ... # pyright: ignore[reportIncompleteStub] +def __getattr__(name: str, /) -> Any: ... # pyright: ignore[reportIncompleteStub] diff --git a/uv.lock b/uv.lock index 765f73f..8cffad9 100644 --- a/uv.lock +++ b/uv.lock @@ -8335,6 +8335,7 @@ dev = [ { name = "myst-parser", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ruff" }, { name = "types-python-xlib", version = "0.33.0.20250809", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "types-python-xlib", version = "0.33.0.20260518", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "types-pywin32" }, @@ -8361,6 +8362,7 @@ dev = [ { name = "ewmhlib" }, { name = "mypy", specifier = ">=0.990,<2" }, { name = "myst-parser" }, + { name = "ruff", specifier = ">=0.15.16" }, { name = "types-python-xlib", specifier = ">=0.32" }, { name = "types-pywin32", specifier = ">=305.0.0.3" }, ] @@ -8481,6 +8483,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + [[package]] name = "six" version = "1.17.0"