From 70168821210400a3c22f260fd69938cf40a5c501 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 19 Jun 2026 16:50:42 -0400 Subject: [PATCH] Checked for performance improvements and comprehension simplifications --- mypy.ini | 8 ++ ruff.toml | 20 +++-- src/pywinctl/_pywinctl_linux.py | 95 +++++++++++------------ src/pywinctl/_pywinctl_macos.py | 62 +++++++-------- src/pywinctl/_pywinctl_win.py | 94 +++++++++++----------- typings/Quartz/ImageKit/__init__.pyi | 2 +- typings/Quartz/PDFKit/__init__.pyi | 2 +- typings/Quartz/QuartzCore/__init__.pyi | 12 ++- typings/Quartz/QuartzFilters/__init__.pyi | 2 - typings/Quartz/QuickLookUI/__init__.pyi | 2 +- 10 files changed, 156 insertions(+), 143 deletions(-) diff --git a/mypy.ini b/mypy.ini index 9a93867..d0bb014 100644 --- a/mypy.ini +++ b/mypy.ini @@ -11,3 +11,11 @@ disallow_untyped_defs = False disable_error_code = # https://github.com/python/mypy/issues/6232 (redefinition with correct type) attr-defined, assignment, + +# https://github.com/ronaldoussoren/pyobjc/issues/198 +# https://github.com/ronaldoussoren/pyobjc/issues/417 +# https://github.com/ronaldoussoren/pyobjc/issues/419 +[mypy-objc.*] +follow_untyped_imports = True +[mypy-Quartz.*] +allow_any_generics = True diff --git a/ruff.toml b/ruff.toml index 8ae083e..1a9802a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,17 +3,22 @@ future-annotations = true # https://docs.astral.sh/ruff/rules/ extend-select = [ "ANN2", # flake8-annotations: missing-return-type - "PYI", # flake8-pyi + "C4", # flake8-comprehensions + "F401", # unused-import + "F404", # late-future-import "FA", # flake8-future-annotations + "FLY", # Flynt "ICN", # flake8-import-conventions - "F401", # unused-import - "YTT", # flake8-2020 + "PERF", # Perflint + "PGH", # pygrep-hooks (blanket-* rules) + "PYI", # flake8-pyi + "RUF", # Ruff-specific rules + "SIM9", # flake8-simplify: split-static-string + zip-dict-keys-and-values + "SLOT", # flake8-slots "TC", # flake8-type-checking "TID", # flake8-tidy-imports "UP", # pyupgrade - "RUF", # Ruff-specific rules - "F404", # late-future-import - "PGH", # pygrep-hooks (blanket-* rules) + "YTT", # flake8-2020 ] ignore = [ # Only enforce return types on public functions. Where otherwise mypy infers as Any @@ -34,6 +39,9 @@ ignore = [ "PYI011", # typed-argument-default-in-stub "PYI014", # argument-default-in-stub "PYI053", # string-or-bytes-too-long + # Good to watch for, but often unfixable + # Anyway Python 3.11 introduced "zero cost" exception handling + "PERF203", # try-except-in-loop, # TODO: Consider later "UP031", # printf-string-formatting diff --git a/src/pywinctl/_pywinctl_linux.py b/src/pywinctl/_pywinctl_linux.py index 15504c3..8586b7c 100644 --- a/src/pywinctl/_pywinctl_linux.py +++ b/src/pywinctl/_pywinctl_linux.py @@ -16,11 +16,8 @@ import Xlib.display import Xlib.error -import Xlib.ext -import Xlib.protocol import Xlib.X import Xlib.Xatom -import Xlib.Xutil from ewmhlib import EwmhRoot, EwmhWindow, Props, defaultEwmhRoot from ewmhlib._ewmhlib import _xlibGetAllWindows from pywinbox import Point, Rect, Size, pointInBox @@ -129,7 +126,7 @@ def getAllTitles() -> list[str]: return [window.title for window in getAllWindows()] -def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | None = (), 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) -> list[LinuxWindow]: """ 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,25 +151,26 @@ def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | Non :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of Window objects """ - matches: list[LinuxWindow] = [] - if title and condition in Re._cond_dic: - lower = False - if condition in (Re.MATCH, Re.NOTMATCH): - title = re.compile(title, flags) - elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): - if not isinstance(flags, int) or not (0 < flags <= 100): - flags = 90 - elif flags == Re.IGNORECASE: - lower = True - if isinstance(title, re.Pattern): - title = title.pattern - else: - title = title.lower() - for win in getAllWindows(): - if win.title and Re._cond_dic[condition](title, win.title.lower() if lower else win.title, flags) \ - and (not app or (app and win.getAppName() in app)): - matches.append(win) - return matches + if not (title and condition in Re._cond_dic): + return [] + + lower = False + if condition in (Re.MATCH, Re.NOTMATCH): + title = re.compile(title, flags) + elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): + if not isinstance(flags, int) or not (0 < flags <= 100): + flags = 90 + elif flags == Re.IGNORECASE: + lower = True + if isinstance(title, re.Pattern): + title = title.pattern + else: + title = title.lower() + return [ + win for win in getAllWindows() + if win.title and Re._cond_dic[condition](title, win.title.lower() if lower else win.title, flags) + and (not app or win.getAppName() in app) + ] def getAllAppsNames() -> list[str]: @@ -184,7 +182,7 @@ def getAllAppsNames() -> list[str]: return list(getAllAppsWindowsTitles()) -def getAppsWithName(name: 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) -> 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) @@ -208,24 +206,25 @@ def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of app names """ - matches: list[str] = [] - if name and condition in Re._cond_dic: - lower = False - if condition in (Re.MATCH, Re.NOTMATCH): - name = re.compile(name, flags) - elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): - if not isinstance(flags, int) or not (0 < flags <= 100): - flags = 90 - elif flags == Re.IGNORECASE: - lower = True - if isinstance(name, re.Pattern): - name = name.pattern - else: - name = name.lower() - for title in getAllAppsNames(): - if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags): - matches.append(title) - return matches + if not (name and condition in Re._cond_dic): + return [] + + lower = False + if condition in (Re.MATCH, Re.NOTMATCH): + name = re.compile(name, flags) + elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): + if not isinstance(flags, int) or not (0 < flags <= 100): + flags = 90 + elif flags == Re.IGNORECASE: + lower = True + if isinstance(name, re.Pattern): + name = name.pattern + else: + name = name.lower() + return [ + title for title in getAllAppsNames() + if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags) + ] def _getAllApps(): @@ -309,7 +308,7 @@ def getAllWindowsDict(tryToFilter: bool = False) -> dict[str, _WINDICT]: return result -def getWindowsAt(x: int, y: int): +def getWindowsAt(x: int, y: int) -> list[LinuxWindow]: """ Get the list of Window objects whose windows contain the point ``(x, y)`` on screen @@ -317,11 +316,11 @@ def getWindowsAt(x: int, y: int): :param y: Y screen coordinate of the window(s) :return: list of Window objects """ - windowBoxGenerator = ((window, window.box) for window in getAllWindows()) return [ - window for (window, box) - in windowBoxGenerator - if pointInBox(x, y, box)] + window for window + in getAllWindows() + if pointInBox(x, y, window.box) + ] def getTopWindowAt(x: int, y: int): @@ -742,7 +741,7 @@ def acceptInput(self, setTo: bool): ret = self._win.getProperty("_MOTIF_WM_HINTS") # Cinnamon uses this as default: [2, 1, 1, 0, 0] - self._motifHints = [a for a in ret.value] if ret and hasattr(ret, "value") else [2, 0, 0, 0, 0] + self._motifHints = list(ret.value) if ret and hasattr(ret, "value") else [2, 0, 0, 0, 0] self._win.changeProperty("_MOTIF_WM_HINTS", [0, 0, 0, 0, 0]) self._win.setWmWindowType(Props.WindowType.DESKTOP) diff --git a/src/pywinctl/_pywinctl_macos.py b/src/pywinctl/_pywinctl_macos.py index 9f19319..b83dc82 100644 --- a/src/pywinctl/_pywinctl_macos.py +++ b/src/pywinctl/_pywinctl_macos.py @@ -159,12 +159,7 @@ def getAllTitles() -> list[str]: .replace('missing value', '"missing value"') \ .replace("{", "[").replace("}", "]") res = ast.literal_eval(ret) - matches: list[str] = [] - if len(res) > 0: - for item in res[0]: - for title in item: - matches.append(title) - return matches + return [title for item in res[0] for title in item] if res else [] def getWindowsWithTitle(title: str | re.Pattern[str], condition: int = Re.IS, flags: int = 0) -> list[MacOSWindow]: @@ -241,7 +236,7 @@ def getAllAppsNames() -> list[str]: return res or [] -def getAppsWithName(name: 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) -> 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) @@ -265,23 +260,24 @@ def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of app names """ - matches: list[str] = [] - if name and condition in Re._cond_dic: - lower = False - if condition in (Re.MATCH, Re.NOTMATCH): - name = re.compile(name, flags) - elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): - if not isinstance(flags, int) or not (0 < flags <= 100): - flags = 90 - elif flags == Re.IGNORECASE: - lower = True - if isinstance(name, re.Pattern): - name = name.pattern - name = name.lower() - for title in getAllAppsNames(): - if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags): - matches.append(title) - return matches + if not (name and condition in Re._cond_dic): + return [] + + lower = False + if condition in (Re.MATCH, Re.NOTMATCH): + name = re.compile(name, flags) + elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): + if not isinstance(flags, int) or not (0 < flags <= 100): + flags = 90 + elif flags == Re.IGNORECASE: + lower = True + if isinstance(name, re.Pattern): + name = name.pattern + name = name.lower() + return [ + title for title in getAllAppsNames() + if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags) + ] def getAllAppsWindowsTitles(): @@ -387,12 +383,11 @@ def getWindowsAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None) -> :param allWindows: (optional) list of window objects (required to improve performance in Apple Script version) :return: list of Window objects """ - windows = allWindows if allWindows else getAllWindows() - windowBoxGenerator = ((window, window.box) for window in windows) return [ - window for (window, box) - in windowBoxGenerator - if pointInBox(x, y, box)] + window for window + in (allWindows or getAllWindows()) + if pointInBox(x, y, window.box) + ] def getTopWindowAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None) -> MacOSWindow | None: @@ -413,11 +408,10 @@ def getTopWindowAt(x: int, y: int, allWindows: list[MacOSWindow] | None = None) def _getAllApps(userOnly: bool = True): - matches: list[AppKit.NSRunningApplication] = [] - for app in AppKit.NSWorkspace.sharedWorkspace().runningApplications(): - if not userOnly or (userOnly and app.activationPolicy() == Quartz.NSApplicationActivationPolicyRegular): - matches.append(app) - return matches + return [ + app for app in AppKit.NSWorkspace.sharedWorkspace().runningApplications() + if not userOnly or app.activationPolicy() == Quartz.NSApplicationActivationPolicyRegular + ] def _getAppWindowsTitles(app: AppKit.NSRunningApplication): diff --git a/src/pywinctl/_pywinctl_win.py b/src/pywinctl/_pywinctl_win.py index 5ef8f67..c725939 100644 --- a/src/pywinctl/_pywinctl_win.py +++ b/src/pywinctl/_pywinctl_win.py @@ -15,8 +15,6 @@ from typing import cast, Any, TYPE_CHECKING from typing_extensions import NotRequired, TypedDict -if TYPE_CHECKING: - from win32.lib.win32gui_struct import _MENUITEMINFO, _MENUINFO import win32gui_struct import win32process @@ -28,6 +26,9 @@ from ._main import BaseWindow, Re, _WatchDog, _findMonitorName, _WINDATA, _WINDICT from pywinbox import Size, Point, Rect, pointInBox +if TYPE_CHECKING: + from win32.lib.win32gui_struct import _MENUITEMINFO, _MENUINFO + # 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. WAIT_ATTEMPTS = 10 @@ -80,13 +81,13 @@ def getAllWindows() -> list[Win32Window]: """ # https://stackoverflow.com/questions/64586371/filtering-background-processes-pywin32 # return [Win32Window(hwnd[0]) for hwnd in _findMainWindowHandles()] - return [window for window in __remove_bad_windows(_findWindowHandles())] + return __remove_bad_windows(_findWindowHandles()) -def __remove_bad_windows(windows: list[int] | None): +def __remove_bad_windows(windows: list[int] | None) -> list[Win32Window]: """ :param windows: win32 Windows - :return: A generator of Win32Window that filters out BadWindows + :return: A list of Win32Window that filters out BadWindows """ outList = [] if windows is not None: @@ -132,25 +133,26 @@ def getWindowsWithTitle(title: str | re.Pattern[str], app: tuple[str, ...] | Non :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of Window objects """ - matches: list[Win32Window] = [] - if title and condition in Re._cond_dic: - lower = False - if condition in (Re.MATCH, Re.NOTMATCH): - title = re.compile(title, flags) - elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): - # flags = Re.IGNORECASE | ratio -> lower = flags & Re.IGNORECASE == Re.IGNORECASE / ratio = flags ^ Re.IGNORECASE - if not isinstance(flags, int) or not (0 < flags <= 100): - flags = 90 - elif flags == Re.IGNORECASE: - lower = True - if isinstance(title, re.Pattern): - title = title.pattern - title = title.lower() - for win in getAllWindows(): - if win.title and Re._cond_dic[condition](title, win.title.lower() if lower else win.title, flags) \ - and (not app or (app and win.getAppName() in app)): - matches.append(win) - return matches + if not (title and condition in Re._cond_dic): + return [] + + lower = False + if condition in (Re.MATCH, Re.NOTMATCH): + title = re.compile(title, flags) + elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): + # flags = Re.IGNORECASE | ratio -> lower = flags & Re.IGNORECASE == Re.IGNORECASE / ratio = flags ^ Re.IGNORECASE + if not isinstance(flags, int) or not (0 < flags <= 100): + flags = 90 + elif flags == Re.IGNORECASE: + lower = True + if isinstance(title, re.Pattern): + title = title.pattern + title = title.lower() + return [ + win for win in getAllWindows() + if win.title and Re._cond_dic[condition](title, win.title.lower() if lower else win.title, flags) + and (not app or win.getAppName() in app) + ] def getAllAppsNames() -> list[str]: @@ -186,24 +188,24 @@ def getAppsWithName(name: str | re.Pattern[str], condition: int = Re.IS, flags: :param flags: (optional) specific flags to apply to condition. Defaults to 0 (no flags) :return: list of app names """ - matches: list[str] = [] - if name and condition in Re._cond_dic: - lower = False - if condition in (Re.MATCH, Re.NOTMATCH): - name = re.compile(name, flags) - elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): - if not isinstance(flags, int) or not (0 < flags <= 100): - flags = 90 - elif flags == Re.IGNORECASE: - lower = True - if isinstance(name, re.Pattern): - name = name.pattern - name = name.lower() - for title in getAllAppsNames(): - if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags): - matches.append(title) - return matches - + if not (name and condition in Re._cond_dic): + return [] + + lower = False + if condition in (Re.MATCH, Re.NOTMATCH): + name = re.compile(name, flags) + elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO): + if not isinstance(flags, int) or not (0 < flags <= 100): + flags = 90 + elif flags == Re.IGNORECASE: + lower = True + if isinstance(name, re.Pattern): + name = name.pattern + name = name.lower() + return [ + title for title in getAllAppsNames() + if title and Re._cond_dic[condition](name, title.lower() if lower else title, flags) + ] def getAllAppsWindowsTitles() -> dict[str, list[str]]: """ @@ -292,11 +294,11 @@ def getWindowsAt(x: int, y: int) -> list[Win32Window]: :param y: Y screen coordinate of the window(s) :return: list of Window objects """ - windowBoxGenerator = ((window, window.box) for window in getAllWindows()) return [ - window for (window, box) - in windowBoxGenerator - if pointInBox(x, y, box)] + window for window + in getAllWindows() + if pointInBox(x, y, window.box) + ] def getTopWindowAt(x: int, y: int) -> Win32Window | None: diff --git a/typings/Quartz/ImageKit/__init__.pyi b/typings/Quartz/ImageKit/__init__.pyi index 28adbb4..e1c1a28 100644 --- a/typings/Quartz/ImageKit/__init__.pyi +++ b/typings/Quartz/ImageKit/__init__.pyi @@ -14,7 +14,7 @@ IKScannerDeviceViewTransferMode: Any _ObjCLazyModule__aliases_deprecated: dict _ObjCLazyModule__enum_deprecated: dict _ObjCLazyModule__expressions: dict -_ObjCLazyModule__expressions_mapping: objc._lazyimport.GetAttrMap +_ObjCLazyModule__expressions_mapping: objc._lazyimport._GetAttrMap _ObjCLazyModule__varmap_dct: dict _ObjCLazyModule__varmap_deprecated: dict diff --git a/typings/Quartz/PDFKit/__init__.pyi b/typings/Quartz/PDFKit/__init__.pyi index 62c2c2a..c17c89b 100644 --- a/typings/Quartz/PDFKit/__init__.pyi +++ b/typings/Quartz/PDFKit/__init__.pyi @@ -34,7 +34,7 @@ PDFWidgetControlType: Any _ObjCLazyModule__aliases_deprecated: dict _ObjCLazyModule__enum_deprecated: dict _ObjCLazyModule__expressions: dict -_ObjCLazyModule__expressions_mapping: objc._lazyimport.GetAttrMap +_ObjCLazyModule__expressions_mapping: objc._lazyimport._GetAttrMap _ObjCLazyModule__varmap_dct: dict _ObjCLazyModule__varmap_deprecated: dict diff --git a/typings/Quartz/QuartzCore/__init__.pyi b/typings/Quartz/QuartzCore/__init__.pyi index b5444a7..75051fd 100644 --- a/typings/Quartz/QuartzCore/__init__.pyi +++ b/typings/Quartz/QuartzCore/__init__.pyi @@ -2,11 +2,15 @@ import sys assert sys.platform == "darwin" -from typing import Any, ClassVar +from typing import Any, ClassVar, type_check_only import objc import objc._lazyimport +# Is actually objc._structwrapper, but our stubs doesn't expose it +@type_check_only +class _objc_structwrapper: ... + CAAnimationCalculationMode: Any CAAnimationRotationMode: Any CAAutoresizingMask: Any @@ -35,7 +39,7 @@ CAValueFunctionName: Any _ObjCLazyModule__aliases_deprecated: dict _ObjCLazyModule__enum_deprecated: dict _ObjCLazyModule__expressions: dict -_ObjCLazyModule__expressions_mapping: objc._lazyimport.GetAttrMap +_ObjCLazyModule__expressions_mapping: objc._lazyimport._GetAttrMap _ObjCLazyModule__varmap_dct: dict _ObjCLazyModule__varmap_deprecated: dict @@ -51,7 +55,7 @@ r: Any protocols: Any expressions: Any -class CAFrameRateRange(objc._structwrapper): +class CAFrameRateRange(_objc_structwrapper): _fields: ClassVar[tuple] = ... __match_args__: ClassVar[tuple] = ... __typestr__: ClassVar[bytes] = ... @@ -70,7 +74,7 @@ class CAFrameRateRange(objc._structwrapper): def __setattr__(self, name, value) -> Any: ... def __setitem__(self, index, object) -> Any: ... -class CATransform3D(objc._structwrapper): +class CATransform3D(_objc_structwrapper): _fields: ClassVar[tuple] = ... __match_args__: ClassVar[tuple] = ... __typestr__: ClassVar[bytes] = ... diff --git a/typings/Quartz/QuartzFilters/__init__.pyi b/typings/Quartz/QuartzFilters/__init__.pyi index 5c5037d..420c7a3 100644 --- a/typings/Quartz/QuartzFilters/__init__.pyi +++ b/typings/Quartz/QuartzFilters/__init__.pyi @@ -4,8 +4,6 @@ assert sys.platform == "darwin" from typing import Any -import Foundation as Foundation - def sel32or64(a, b): ... def selAorI(a, b): ... diff --git a/typings/Quartz/QuickLookUI/__init__.pyi b/typings/Quartz/QuickLookUI/__init__.pyi index 50df409..389044d 100644 --- a/typings/Quartz/QuickLookUI/__init__.pyi +++ b/typings/Quartz/QuickLookUI/__init__.pyi @@ -10,7 +10,7 @@ QLPreviewViewStyle: Any _ObjCLazyModule__aliases_deprecated: dict _ObjCLazyModule__enum_deprecated: dict _ObjCLazyModule__expressions: dict -_ObjCLazyModule__expressions_mapping: objc._lazyimport.GetAttrMap +_ObjCLazyModule__expressions_mapping: objc._lazyimport._GetAttrMap _ObjCLazyModule__varmap_dct: dict _ObjCLazyModule__varmap_deprecated: dict