From bfc22787537c98332a2c840d1b47b87b84186017 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 20 Oct 2023 13:16:43 -0400 Subject: [PATCH 1/6] Config changes --- .pre-commit-config.yaml | 7 ++----- .vscode/extensions.json | 6 +++--- .vscode/settings.json | 6 ++++-- README.md | 2 +- pyproject.toml | 33 ++++++++++----------------------- scripts/lint.ps1 | 2 +- scripts/requirements-dev.txt | 3 +-- 7 files changed, 22 insertions(+), 37 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7e94e8c..71d6c402 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,14 +17,11 @@ repos: - id: pretty-format-ini args: [--autofix] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.1.14" # Must match requirements-dev.txt + rev: "v0.2.0" # Must match requirements-dev.txt hooks: - id: ruff + - id: ruff-format args: [--fix] - - repo: https://github.com/hhatto/autopep8 - rev: "v2.0.4" # Must match requirements-dev.txt - hooks: - - id: autopep8 - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 # Must match requirements-dev.txt hooks: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 790ee33f..a7722a2b 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,11 +1,11 @@ // Keep in alphabetical order { "recommendations": [ + "charliermarsh.ruff", "davidanson.vscode-markdownlint", "eamodio.gitlens", "emeraldwalk.runonsave", "github.vscode-github-actions", - "ms-python.autopep8", "ms-python.python", "ms-python.vscode-pylance", "ms-vscode.powershell", @@ -32,11 +32,11 @@ // Don't recommend to autoinstall // // // Use Ruff instead + "ms-python.autopep8", + "ms-python.black-formatter", "ms-python.flake8", "ms-python.isort", "ms-python.pylint", - // We use autopep8 - "ms-python.black-formatter", // This is a Git project "johnstoncode.svn-scm", // Prefer using VSCode itself as a text editor diff --git a/.vscode/settings.json b/.vscode/settings.json index 815e9679..6fa16e0e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,8 +63,7 @@ "editor.defaultFormatter": "vscode.json-language-features", }, "[python]": { - // Ruff as a formatter doesn't fully satisfy our needs yet: https://github.com/astral-sh/ruff/discussions/7310 - "editor.defaultFormatter": "ms-python.autopep8", + "editor.defaultFormatter": "charliermarsh.ruff", "editor.tabSize": 4, "editor.rulers": [ 72, // PEP8-17 docstrings @@ -87,6 +86,9 @@ ], "python.analysis.diagnosticMode": "workspace", "ruff.importStrategy": "fromEnvironment", + "ruff.enable": true, + "ruff.fixAll": true, + "ruff.organizeImports": true, // Use the Ruff extension instead "isort.check": false, "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", diff --git a/README.md b/README.md index 298e7d7d..696c974b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Avasam_AutoSplit&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Avasam_AutoSplit) [![SemVer](https://badgen.net/badge/_/SemVer%20compliant/grey?label)](https://semver.org/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![autopep8](https://badgen.net/badge/code%20style/autopep8/blue)](https://github.com/hhatto/autopep8) +[![Ruff format](https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json)](https://docs.astral.sh/ruff/settings/#format) [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/) [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) diff --git a/pyproject.toml b/pyproject.toml index 32dd3d02..8d2587db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,13 @@ select = ["ALL"] preview = true # https://docs.astral.sh/ruff/rules/ ignore = [ + ### + # Conflicts with ruff-format + ### + "COM812", # missing-trailing-comma + "E203", # whitespace-before-punctuation + "ISC001", # single-line-implicit-string-concatenation + ### # Not needed or wanted ### @@ -65,6 +72,9 @@ ignore = [ "EXE", # flake8-executable ] +[tool.ruff.format] +docstring-code-format = true + [tool.ruff.per-file-ignores] "typings/**/*.pyi" = [ "F811", # Re-exports false positives @@ -120,29 +130,6 @@ max-args = 7 # At least same as max-complexity max-branches = 15 -# https://github.com/hhatto/autopep8#usage -# https://github.com/hhatto/autopep8#more-advanced-usage -[tool.autopep8] -max_line_length = 120 -aggressive = 3 -exclude = ".venv/*,src/gen/*" -ignore = [ - "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma) - "E70", # Allow ... on same line as def - "E721", # Breaks when needing an exact type - # Autofixed by Ruff - # Check for the "Fix" flag https://docs.astral.sh/ruff/rules/#pycodestyle-e-w - "E2", # Whitespace - "E703", # useless-semicolon - "E71", # Statement (comparisons) - "E731", # lambda-assignment - "W29", # Whitespace warning - "W605", # invalid-escape-sequence - # Autofixed by other Ruff rules - "E401", # I001: unsorted-imports - "W690", # UP: pyupgrade -] - # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file [tool.pyright] typeCheckingMode = "strict" diff --git a/scripts/lint.ps1 b/scripts/lint.ps1 index 84dc6f8b..9e787e7d 100644 --- a/scripts/lint.ps1 +++ b/scripts/lint.ps1 @@ -3,7 +3,7 @@ Set-Location "$PSScriptRoot/.." $exitCodes = 0 Write-Host "`nRunning formatting..." -autopep8 src/ --recursive --in-place +ruff format . add-trailing-comma $(git ls-files '**.py*') Write-Host "`nRunning Ruff..." diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index 370084ba..4d018ace 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -13,8 +13,7 @@ # # Linters & Formatters add-trailing-comma>=3.1.0 # Must match .pre-commit-config.yaml -autopep8>=2.0.4 # Must match .pre-commit-config.yaml -ruff>=0.1.14 # New checks # Must match .pre-commit-config.yaml +ruff>=0.2.0 # Split lint settings # Must match .pre-commit-config.yaml # # Types types-D3DShot ; sys_platform == 'win32' From 03ff5ca1225c7a797740e9da67adea9383bd95df Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 2 Feb 2024 19:43:28 -0500 Subject: [PATCH 2/6] Ruff acceptable changes + acceptable changes --- src/AutoSplit.py | 1 + src/error_messages.py | 23 +++++++++++++---------- src/user_profile.py | 3 +-- src/utils.py | 3 ++- typings/scipy/__init__.pyi | 2 +- typings/scipy/fft/_realtransforms.pyi | 15 --------------- 6 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 6e190be9..c3a98e48 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -53,6 +53,7 @@ if sys.platform == "win32": from win32comext.shell import shell as shell32 + myappid = f"Toufool.AutoSplit.v{AUTOSPLIT_VERSION}" shell32.SetCurrentProcessExplicitAppUserModelID(myappid) diff --git a/src/error_messages.py b/src/error_messages.py index 6cf64805..915661dc 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -154,14 +154,15 @@ def linux_groups(): "Linux users must ensure they are in the 'tty' and 'input' groups " + "and have write access to '/dev/uinput'. You can run the following commands to do so:", # Keep in sync with README.md and scripts/install.ps1 - "sudo usermod -a -G tty,input $USER" - + "\nsudo touch /dev/uinput" - + "\nsudo chmod +0666 /dev/uinput" - + "\necho 'KERNEL==\"uinput\", TAG+=\"uaccess\"' | sudo tee /etc/udev/rules.d/50-uinput.rules" - + "\necho 'SUBSYSTEM==\"input\", MODE=\"0666\" GROUP=\"plugdev\"' | sudo tee /etc/udev/rules.d/12-input.rules" - + "\necho 'SUBSYSTEM==\"misc\", MODE=\"0666\" GROUP=\"plugdev\"' | sudo tee -a /etc/udev/rules.d/12-input.rules" - + "\necho 'SUBSYSTEM==\"tty\", MODE=\"0666\" GROUP=\"plugdev\"' | sudo tee -a /etc/udev/rules.d/12-input.rules" - + "\nloginctl terminate-user $USER", + """\ +sudo usermod -a -G tty,input $USER +sudo touch /dev/uinput +sudo chmod +0666 /dev/uinput +echo 'KERNEL=="uinput", TAG+="uaccess"' | sudo tee /etc/udev/rules.d/50-uinput.rules +echo 'SUBSYSTEM=="input", MODE="0666" GROUP="plugdev"' | sudo tee /etc/udev/rules.d/12-input.rules +echo 'SUBSYSTEM=="misc", MODE="0666" GROUP="plugdev"' | sudo tee -a /etc/udev/rules.d/12-input.rules +echo 'SUBSYSTEM=="tty", MODE="0666" GROUP="plugdev"' | sudo tee -a /etc/udev/rules.d/12-input.rules +loginctl terminate-user $USER""", ) @@ -174,9 +175,11 @@ def linux_uinput(): # Keep in sync with README.md#DOWNLOAD_AND_OPEN -WAYLAND_WARNING = "All screen capture method are incompatible with Wayland. Follow this guide to disable it: " \ - + '\n' \ +WAYLAND_WARNING = ( + "All screen capture method are incompatible with Wayland. Follow this guide to disable it: " + + '\n' + "https://linuxconfig.org/how-to-enable-disable-wayland-on-ubuntu-22-04-desktop" +) def linux_wayland(): diff --git a/src/user_profile.py b/src/user_profile.py index 14485121..3780e18e 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -96,8 +96,7 @@ def save_settings_as(autosplit: "AutoSplit"): save_settings_file_path = QtWidgets.QFileDialog.getSaveFileName( autosplit, "Save Settings As", - autosplit.last_successfully_loaded_settings_file_path - or os.path.join(auto_split_directory, "settings.toml"), + autosplit.last_successfully_loaded_settings_file_path or os.path.join(auto_split_directory, "settings.toml"), "TOML (*.toml)", )[0] diff --git a/src/utils.py b/src/utils.py index 047befbd..46dcfb19 100644 --- a/src/utils.py +++ b/src/utils.py @@ -129,7 +129,7 @@ def open_file(file_path: str | bytes | os.PathLike[str] | os.PathLike[bytes]): import subprocess # noqa: PLC0415, S404 opener = "xdg-open" if sys.platform == "linux" else "open" - subprocess.call([opener, file_path]) # noqa: S603 + subprocess.call([opener, file_path]) # noqa: S603 def get_or_create_eventloop(): @@ -151,6 +151,7 @@ def get_direct3d_device(): async def init_mediacapture(): await media_capture.initialize_async() + asyncio.run(init_mediacapture()) direct_3d_device = media_capture.media_capture_settings and media_capture.media_capture_settings.direct3_d11_device if not direct_3d_device: diff --git a/typings/scipy/__init__.pyi b/typings/scipy/__init__.pyi index 8e85a093..4b94936a 100644 --- a/typings/scipy/__init__.pyi +++ b/typings/scipy/__init__.pyi @@ -1,4 +1,3 @@ - from numpy.fft import ifft as ifft from numpy.random import rand as rand, randn as randn from scipy import ( @@ -52,4 +51,5 @@ __all__ = [ ] test: PytestTester + def __dir__() -> list[str]: ... diff --git a/typings/scipy/fft/_realtransforms.pyi b/typings/scipy/fft/_realtransforms.pyi index 7c28f8a8..543784d2 100644 --- a/typings/scipy/fft/_realtransforms.pyi +++ b/typings/scipy/fft/_realtransforms.pyi @@ -4,7 +4,6 @@ from numpy.typing import NDArray __all__ = ["dct", "dctn", "dst", "dstn", "idct", "idctn", "idst", "idstn"] - def dctn( x, type=2, @@ -16,8 +15,6 @@ def dctn( *, orthogonalize=None, ): ... - - def idctn( x, type=2, @@ -28,8 +25,6 @@ def idctn( workers=None, orthogonalize=None, ): ... - - def dstn( x, type=2, @@ -40,8 +35,6 @@ def dstn( workers=None, orthogonalize=None, ): ... - - def idstn( x, type=2, @@ -52,8 +45,6 @@ def idstn( workers=None, orthogonalize=None, ): ... - - def dct( x: NDArray[generic], type: int = 2, @@ -64,8 +55,6 @@ def dct( workers: Incomplete | None = None, orthogonalize: Incomplete | None = None, ) -> NDArray[float64]: ... - - def idct( x, type=2, @@ -76,8 +65,6 @@ def idct( workers=None, orthogonalize=None, ): ... - - def dst( x, type=2, @@ -88,8 +75,6 @@ def dst( workers=None, orthogonalize=None, ): ... - - def idst( x, type=2, From 832501326d4455248a14a47ffa95b1e224f50e41 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 2 Feb 2024 19:44:19 -0500 Subject: [PATCH 3/6] Ruff unnacceptable changes --- src/AutoSplit.py | 19 +++---------- .../DesktopDuplicationCaptureMethod.py | 4 +-- src/capture_method/ScrotCaptureMethod.py | 3 +- .../VideoCaptureDeviceCaptureMethod.py | 3 +- .../WindowsGraphicsCaptureMethod.py | 4 +-- src/capture_method/__init__.py | 9 ++---- src/compare.py | 10 ++----- src/hotkeys.py | 28 ++++--------------- src/split_parser.py | 15 ++-------- src/user_profile.py | 6 +--- 10 files changed, 22 insertions(+), 79 deletions(-) diff --git a/src/AutoSplit.py b/src/AutoSplit.py index c3a98e48..5643d40e 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -127,8 +127,7 @@ def _show_error_signal_slot(error_message_box: Callable[..., object]): self.setupUi(self) self.setWindowTitle( - f"AutoSplit v{AUTOSPLIT_VERSION}" + - (" (externally controlled)" if self.is_auto_controlled else ""), + f"AutoSplit v{AUTOSPLIT_VERSION}" + (" (externally controlled)" if self.is_auto_controlled else ""), ) # Hotkeys need to be initialized to be passed as thread arguments in hotkeys.py @@ -441,10 +440,7 @@ def __check_fps(self): self.fps_value_label.setText(str(fps)) def __is_current_split_out_of_range(self): - return ( - self.split_image_number < 0 - or self.split_image_number > len(self.split_images_and_loop_number) - 1 - ) + return self.split_image_number < 0 or self.split_image_number > len(self.split_images_and_loop_number) - 1 def undo_split(self, navigate_image_only: bool = False): """Undo Split" and "Prev. Img." buttons connect to here.""" @@ -508,10 +504,7 @@ def reset(self): # Functions for the hotkeys to return to the main thread from signals and start their corresponding functions def start_auto_splitter(self): # If the auto splitter is already running or the button is disabled, don't emit the signal to start it. - if ( - self.is_running - or (not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled) - ): + if self.is_running or (not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled): return start_label = self.start_image_status_value_label.text() @@ -544,11 +537,7 @@ def __auto_splitter(self): # noqa: PLR0912,PLR0915 # Construct a list of images + loop count tuples. self.split_images_and_loop_number = list( - flatten( - ((split_image, i + 1) for i in range(split_image.loops)) - for split_image - in self.split_images - ), + flatten(((split_image, i + 1) for i in range(split_image.loops)) for split_image in self.split_images), ) # Construct groups of splits diff --git a/src/capture_method/DesktopDuplicationCaptureMethod.py b/src/capture_method/DesktopDuplicationCaptureMethod.py index de57f13d..8a4bfc31 100644 --- a/src/capture_method/DesktopDuplicationCaptureMethod.py +++ b/src/capture_method/DesktopDuplicationCaptureMethod.py @@ -47,9 +47,7 @@ def get_frame(self): left_bounds, top_bounds, *_ = get_window_bounds(hwnd) self.desktop_duplication.display = next( - display for display - in self.desktop_duplication.displays - if display.hmonitor == hmonitor + display for display in self.desktop_duplication.displays if display.hmonitor == hmonitor ) offset_x, offset_y, *_ = win32gui.GetWindowRect(hwnd) offset_x -= self.desktop_duplication.display.position["left"] diff --git a/src/capture_method/ScrotCaptureMethod.py b/src/capture_method/ScrotCaptureMethod.py index 20ed9375..9b95c79d 100644 --- a/src/capture_method/ScrotCaptureMethod.py +++ b/src/capture_method/ScrotCaptureMethod.py @@ -19,8 +19,7 @@ class ScrotCaptureMethod(CaptureMethodBase): name = "Scrot" short_description = "very slow, may leave files" description = ( - "\nUses Scrot (SCReenshOT) to take screenshots. " - + "\nLeaves behind a screenshot file if interrupted. " + "\nUses Scrot (SCReenshOT) to take screenshots. " + "\nLeaves behind a screenshot file if interrupted. " ) @override diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py index 1b11d2e3..b3198e92 100644 --- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py +++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py @@ -38,8 +38,7 @@ class VideoCaptureDeviceCaptureMethod(CaptureMethodBase): name = "Video Capture Device" short_description = "see below" description = ( - "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " - + "\nYou can select one below. " + "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " + "\nYou can select one below. " ) capture_device: cv2.VideoCapture diff --git a/src/capture_method/WindowsGraphicsCaptureMethod.py b/src/capture_method/WindowsGraphicsCaptureMethod.py index 3ef3b984..caa93a04 100644 --- a/src/capture_method/WindowsGraphicsCaptureMethod.py +++ b/src/capture_method/WindowsGraphicsCaptureMethod.py @@ -154,7 +154,5 @@ def recover_window(self, captured_window_title: str): @override def check_selected_region_exists(self): return bool( - is_valid_hwnd(self._autosplit_ref.hwnd) - and self.frame_pool - and self.session, + is_valid_hwnd(self._autosplit_ref.hwnd) and self.frame_pool and self.session, ) diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 8569a197..c8c7a6dd 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -241,15 +241,10 @@ async def get_camera_info(index: int, device_name: str): # video_capture.release() resolution = get_input_device_resolution(index) - return ( - CameraInfo(index, device_name, False, backend, resolution) - if resolution is not None - else None - ) + return CameraInfo(index, device_name, False, backend, resolution) if resolution is not None else None return [ camera_info - for camera_info - in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) + for camera_info in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) if camera_info is not None ] diff --git a/src/compare.py b/src/compare.py index 49bec7e6..d79c6b41 100644 --- a/src/compare.py +++ b/src/compare.py @@ -45,9 +45,7 @@ def compare_l2_norm(source: MatLike, capture: MatLike, mask: MatLike | None = No # The L2 Error is summed across all pixels, so this normalizes max_error = ( - sqrt(source.size) * MAXBYTE - if not is_valid_image(mask) - else sqrt(cv2.countNonZero(mask) * MASK_SIZE_MULTIPLIER) + sqrt(source.size) * MAXBYTE if not is_valid_image(mask) else sqrt(cv2.countNonZero(mask) * MASK_SIZE_MULTIPLIER) ) if not max_error: @@ -71,11 +69,7 @@ def compare_template(source: MatLike, capture: MatLike, mask: MatLike | None = N # matchTemplate returns the sum of square differences, this is the max # that the value can be. Used for normalizing from 0 to 1. - max_error = ( - source.size * MAXBYTE * MAXBYTE - if not is_valid_image(mask) - else cv2.countNonZero(mask) - ) + max_error = (source.size * MAXBYTE * MAXBYTE) if not is_valid_image(mask) else cv2.countNonZero(mask) return 1 - (min_val / max_error) diff --git a/src/hotkeys.py b/src/hotkeys.py index e803e4db..c8892012 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -100,10 +100,7 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # Deal with regular inputs # If an int or does not contain the following strings - if ( - isinstance(hotkey_or_scan_code, int) - or not any(key in hotkey_or_scan_code for key in ("num ", "decimal", "+")) - ): + if isinstance(hotkey_or_scan_code, int) or not any(key in hotkey_or_scan_code for key in ("num ", "decimal", "+")): keyboard.send(hotkey_or_scan_code) return @@ -112,11 +109,7 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # keyboard also has issues with capitalization modifier (shift+A) # keyboard.send(keyboard.key_to_scan_codes(key_or_scan_code)[1]) pyautogui.hotkey( - *[ - "+" if key == "plus" else key - for key - in hotkey_or_scan_code.replace(" ", "").split("+") - ], + *["+" if key == "plus" else key for key in hotkey_or_scan_code.replace(" ", "").split("+")], ) @@ -139,9 +132,7 @@ def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) if keyboard_event.name and is_digit(keyboard_event.name[-1]): # Prevent "regular numbers" and "keypad numbers" from activating each other return bool( - keyboard_event.is_keypad - if expected_key.startswith("num ") - else not keyboard_event.is_keypad, + keyboard_event.is_keypad if expected_key.startswith("num ") else not keyboard_event.is_keypad, ) # Prevent "keypad action keys" from triggering "regular numbers" and "keypad numbers" @@ -165,11 +156,7 @@ def __get_key_name(keyboard_event: keyboard.KeyboardEvent): # Normally this is done by keyboard.get_hotkey_name. But our code won't always get there. if event_name == "+": return "plus" - return ( - f"num {keyboard_event.name}" - if keyboard_event.is_keypad and is_digit(keyboard_event.name) - else event_name - ) + return f"num {keyboard_event.name}" if keyboard_event.is_keypad and is_digit(keyboard_event.name) else event_name def __get_hotkey_name(names: list[str]): @@ -248,11 +235,8 @@ def toggle_auto_reset_image(): def is_valid_hotkey_name(hotkey_name: str): - return any( - key and not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) - for key - in hotkey_name.split("+") - ) + return any(key and not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) for key in hotkey_name.split("+")) + # TODO: using getattr/setattr is NOT a good way to go about this. It was only temporarily done to # reduce duplicated code. We should use a dictionary of hotkey class or something. diff --git a/src/split_parser.py b/src/split_parser.py index acdf6dd7..87549881 100644 --- a/src/split_parser.py +++ b/src/split_parser.py @@ -176,8 +176,7 @@ def parse_and_validate_images(autosplit: "AutoSplit"): # Get split images all_images = [ AutoSplitImage(os.path.join(autosplit.settings_dict["split_image_directory"], image_name)) - for image_name - in os.listdir(autosplit.settings_dict["split_image_directory"]) + for image_name in os.listdir(autosplit.settings_dict["split_image_directory"]) ] # Find non-split images and then remove them from the list @@ -188,19 +187,11 @@ def parse_and_validate_images(autosplit: "AutoSplit"): error_message: Callable[[], object] | None = None # If there is no start hotkey set but a Start Image is present, and is not auto controlled, throw an error. - if ( - start_image - and not autosplit.settings_dict["split_hotkey"] - and not autosplit.is_auto_controlled - ): + if start_image and not autosplit.settings_dict["split_hotkey"] and not autosplit.is_auto_controlled: error_message = error_messages.load_start_image # If there is no reset hotkey set but a Reset Image is present, and is not auto controlled, throw an error. - elif ( - reset_image - and not autosplit.settings_dict["reset_hotkey"] - and not autosplit.is_auto_controlled - ): + elif reset_image and not autosplit.settings_dict["reset_hotkey"] and not autosplit.is_auto_controlled: error_message = error_messages.reset_hotkey # Make sure that each of the images follows the guidelines for correct format diff --git a/src/user_profile.py b/src/user_profile.py index 3780e18e..047161d8 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -190,11 +190,7 @@ def load_settings(autosplit: "AutoSplit", from_path: str = ""): def load_settings_on_open(autosplit: "AutoSplit"): - settings_files = [ - file for file - in os.listdir(auto_split_directory) - if file.endswith(".toml") - ] + settings_files = [file for file in os.listdir(auto_split_directory) if file.endswith(".toml")] # Find all .tomls in AutoSplit folder, error if there is not exactly 1 error = None From 92f8878a35d141e907661a96baf771b845b31b57 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 2 Feb 2024 20:47:50 -0500 Subject: [PATCH 4/6] Revert "Ruff unnacceptable changes" Use `fmt: skip` comments --- src/AutoSplit.py | 18 ++++++++++--- .../DesktopDuplicationCaptureMethod.py | 4 ++- src/capture_method/ScrotCaptureMethod.py | 3 ++- .../VideoCaptureDeviceCaptureMethod.py | 3 ++- .../WindowsGraphicsCaptureMethod.py | 4 ++- src/capture_method/__init__.py | 8 ++++-- src/compare.py | 10 ++++++-- src/hotkeys.py | 25 +++++++++++++++---- src/split_parser.py | 14 ++++++++--- src/user_profile.py | 6 ++++- 10 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 5643d40e..0dce8710 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -127,7 +127,8 @@ def _show_error_signal_slot(error_message_box: Callable[..., object]): self.setupUi(self) self.setWindowTitle( - f"AutoSplit v{AUTOSPLIT_VERSION}" + (" (externally controlled)" if self.is_auto_controlled else ""), + f"AutoSplit v{AUTOSPLIT_VERSION}" # fmt: skip + + (" (externally controlled)" if self.is_auto_controlled else ""), ) # Hotkeys need to be initialized to be passed as thread arguments in hotkeys.py @@ -440,7 +441,10 @@ def __check_fps(self): self.fps_value_label.setText(str(fps)) def __is_current_split_out_of_range(self): - return self.split_image_number < 0 or self.split_image_number > len(self.split_images_and_loop_number) - 1 + return ( + self.split_image_number < 0 # fmt: skip + or self.split_image_number > len(self.split_images_and_loop_number) - 1 + ) def undo_split(self, navigate_image_only: bool = False): """Undo Split" and "Prev. Img." buttons connect to here.""" @@ -504,7 +508,10 @@ def reset(self): # Functions for the hotkeys to return to the main thread from signals and start their corresponding functions def start_auto_splitter(self): # If the auto splitter is already running or the button is disabled, don't emit the signal to start it. - if self.is_running or (not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled): + if ( + self.is_running # fmt: skip + or (not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled) + ): return start_label = self.start_image_status_value_label.text() @@ -537,7 +544,10 @@ def __auto_splitter(self): # noqa: PLR0912,PLR0915 # Construct a list of images + loop count tuples. self.split_images_and_loop_number = list( - flatten(((split_image, i + 1) for i in range(split_image.loops)) for split_image in self.split_images), + flatten( + ((split_image, i + 1) for i in range(split_image.loops)) # fmt: skip + for split_image in self.split_images + ), ) # Construct groups of splits diff --git a/src/capture_method/DesktopDuplicationCaptureMethod.py b/src/capture_method/DesktopDuplicationCaptureMethod.py index 8a4bfc31..c3fb8f16 100644 --- a/src/capture_method/DesktopDuplicationCaptureMethod.py +++ b/src/capture_method/DesktopDuplicationCaptureMethod.py @@ -47,7 +47,9 @@ def get_frame(self): left_bounds, top_bounds, *_ = get_window_bounds(hwnd) self.desktop_duplication.display = next( - display for display in self.desktop_duplication.displays if display.hmonitor == hmonitor + display + for display in self.desktop_duplication.displays # fmt: skip + if display.hmonitor == hmonitor ) offset_x, offset_y, *_ = win32gui.GetWindowRect(hwnd) offset_x -= self.desktop_duplication.display.position["left"] diff --git a/src/capture_method/ScrotCaptureMethod.py b/src/capture_method/ScrotCaptureMethod.py index 9b95c79d..7041c7c1 100644 --- a/src/capture_method/ScrotCaptureMethod.py +++ b/src/capture_method/ScrotCaptureMethod.py @@ -19,7 +19,8 @@ class ScrotCaptureMethod(CaptureMethodBase): name = "Scrot" short_description = "very slow, may leave files" description = ( - "\nUses Scrot (SCReenshOT) to take screenshots. " + "\nLeaves behind a screenshot file if interrupted. " + "\nUses Scrot (SCReenshOT) to take screenshots. " # fmt: skip + + "\nLeaves behind a screenshot file if interrupted. " ) @override diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py index b3198e92..764695d7 100644 --- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py +++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py @@ -38,7 +38,8 @@ class VideoCaptureDeviceCaptureMethod(CaptureMethodBase): name = "Video Capture Device" short_description = "see below" description = ( - "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " + "\nYou can select one below. " + "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " # fmt: skip + + "\nYou can select one below. " ) capture_device: cv2.VideoCapture diff --git a/src/capture_method/WindowsGraphicsCaptureMethod.py b/src/capture_method/WindowsGraphicsCaptureMethod.py index caa93a04..79610c40 100644 --- a/src/capture_method/WindowsGraphicsCaptureMethod.py +++ b/src/capture_method/WindowsGraphicsCaptureMethod.py @@ -154,5 +154,7 @@ def recover_window(self, captured_window_title: str): @override def check_selected_region_exists(self): return bool( - is_valid_hwnd(self._autosplit_ref.hwnd) and self.frame_pool and self.session, + is_valid_hwnd(self._autosplit_ref.hwnd) # fmt: skip + and self.frame_pool + and self.session, ) diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index c8c7a6dd..a0740f10 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -241,10 +241,14 @@ async def get_camera_info(index: int, device_name: str): # video_capture.release() resolution = get_input_device_resolution(index) - return CameraInfo(index, device_name, False, backend, resolution) if resolution is not None else None + return ( + CameraInfo(index, device_name, False, backend, resolution) # fmt: skip + if resolution is not None + else None + ) return [ camera_info - for camera_info in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) + for camera_info in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) # fmt: skip if camera_info is not None ] diff --git a/src/compare.py b/src/compare.py index d79c6b41..26611805 100644 --- a/src/compare.py +++ b/src/compare.py @@ -45,7 +45,9 @@ def compare_l2_norm(source: MatLike, capture: MatLike, mask: MatLike | None = No # The L2 Error is summed across all pixels, so this normalizes max_error = ( - sqrt(source.size) * MAXBYTE if not is_valid_image(mask) else sqrt(cv2.countNonZero(mask) * MASK_SIZE_MULTIPLIER) + sqrt(source.size) * MAXBYTE # fmt: skip + if not is_valid_image(mask) + else sqrt(cv2.countNonZero(mask) * MASK_SIZE_MULTIPLIER) ) if not max_error: @@ -69,7 +71,11 @@ def compare_template(source: MatLike, capture: MatLike, mask: MatLike | None = N # matchTemplate returns the sum of square differences, this is the max # that the value can be. Used for normalizing from 0 to 1. - max_error = (source.size * MAXBYTE * MAXBYTE) if not is_valid_image(mask) else cv2.countNonZero(mask) + max_error = ( + source.size * MAXBYTE * MAXBYTE # fmt: skip + if not is_valid_image(mask) + else cv2.countNonZero(mask) + ) return 1 - (min_val / max_error) diff --git a/src/hotkeys.py b/src/hotkeys.py index c8892012..895ea398 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -100,7 +100,10 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # Deal with regular inputs # If an int or does not contain the following strings - if isinstance(hotkey_or_scan_code, int) or not any(key in hotkey_or_scan_code for key in ("num ", "decimal", "+")): + if ( + isinstance(hotkey_or_scan_code, int) # fmt: skip + or not any(key in hotkey_or_scan_code for key in ("num ", "decimal", "+")) + ): keyboard.send(hotkey_or_scan_code) return @@ -109,7 +112,10 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # keyboard also has issues with capitalization modifier (shift+A) # keyboard.send(keyboard.key_to_scan_codes(key_or_scan_code)[1]) pyautogui.hotkey( - *["+" if key == "plus" else key for key in hotkey_or_scan_code.replace(" ", "").split("+")], + *[ + "+" if key == "plus" else key # fmt: skip + for key in hotkey_or_scan_code.replace(" ", "").split("+") + ], ) @@ -132,7 +138,9 @@ def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) if keyboard_event.name and is_digit(keyboard_event.name[-1]): # Prevent "regular numbers" and "keypad numbers" from activating each other return bool( - keyboard_event.is_keypad if expected_key.startswith("num ") else not keyboard_event.is_keypad, + keyboard_event.is_keypad # fmt: skip + if expected_key.startswith("num ") + else not keyboard_event.is_keypad, ) # Prevent "keypad action keys" from triggering "regular numbers" and "keypad numbers" @@ -156,7 +164,11 @@ def __get_key_name(keyboard_event: keyboard.KeyboardEvent): # Normally this is done by keyboard.get_hotkey_name. But our code won't always get there. if event_name == "+": return "plus" - return f"num {keyboard_event.name}" if keyboard_event.is_keypad and is_digit(keyboard_event.name) else event_name + return ( + f"num {keyboard_event.name}" # fmt: skip + if keyboard_event.is_keypad and is_digit(keyboard_event.name) + else event_name + ) def __get_hotkey_name(names: list[str]): @@ -235,7 +247,10 @@ def toggle_auto_reset_image(): def is_valid_hotkey_name(hotkey_name: str): - return any(key and not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) for key in hotkey_name.split("+")) + return any( + key and not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) # fmt: skip + for key in hotkey_name.split("+") + ) # TODO: using getattr/setattr is NOT a good way to go about this. It was only temporarily done to diff --git a/src/split_parser.py b/src/split_parser.py index 87549881..09f6496e 100644 --- a/src/split_parser.py +++ b/src/split_parser.py @@ -175,7 +175,7 @@ def __pop_image_type(split_image: list[AutoSplitImage], image_type: ImageType): def parse_and_validate_images(autosplit: "AutoSplit"): # Get split images all_images = [ - AutoSplitImage(os.path.join(autosplit.settings_dict["split_image_directory"], image_name)) + AutoSplitImage(os.path.join(autosplit.settings_dict["split_image_directory"], image_name)) # fmt: skip for image_name in os.listdir(autosplit.settings_dict["split_image_directory"]) ] @@ -187,11 +187,19 @@ def parse_and_validate_images(autosplit: "AutoSplit"): error_message: Callable[[], object] | None = None # If there is no start hotkey set but a Start Image is present, and is not auto controlled, throw an error. - if start_image and not autosplit.settings_dict["split_hotkey"] and not autosplit.is_auto_controlled: + if ( + start_image # fmt: skip + and not autosplit.settings_dict["split_hotkey"] + and not autosplit.is_auto_controlled + ): error_message = error_messages.load_start_image # If there is no reset hotkey set but a Reset Image is present, and is not auto controlled, throw an error. - elif reset_image and not autosplit.settings_dict["reset_hotkey"] and not autosplit.is_auto_controlled: + elif ( + reset_image # fmt: skip + and not autosplit.settings_dict["reset_hotkey"] + and not autosplit.is_auto_controlled + ): error_message = error_messages.reset_hotkey # Make sure that each of the images follows the guidelines for correct format diff --git a/src/user_profile.py b/src/user_profile.py index 047161d8..2218f9d9 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -190,7 +190,11 @@ def load_settings(autosplit: "AutoSplit", from_path: str = ""): def load_settings_on_open(autosplit: "AutoSplit"): - settings_files = [file for file in os.listdir(auto_split_directory) if file.endswith(".toml")] + settings_files = [ + file # fmt: skip + for file in os.listdir(auto_split_directory) + if file.endswith(".toml") + ] # Find all .tomls in AutoSplit folder, error if there is not exactly 1 error = None From aa05e2f0f87503fe113637b298ba1b83dafe2347 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 18 Aug 2024 14:33:55 -0400 Subject: [PATCH 5/6] Update Ruff format to 0.6 and 100 line length --- .pre-commit-config.yaml | 4 - .vscode/settings.json | 8 - ruff.toml | 4 +- scripts/lint.ps1 | 5 +- scripts/requirements-dev.txt | 1 - src/AutoSplit.py | 78 ++++----- src/AutoSplitImage.py | 12 +- .../DesktopDuplicationCaptureMethod.py | 4 +- src/capture_method/ScrotCaptureMethod.py | 2 +- .../VideoCaptureDeviceCaptureMethod.py | 6 +- .../WindowsGraphicsCaptureMethod.py | 6 +- src/capture_method/__init__.py | 6 +- src/compare.py | 6 +- src/error_messages.py | 26 +-- src/hotkeys.py | 37 +++-- src/menu_bar.py | 153 ++++++++++-------- src/region_selection.py | 4 +- src/split_parser.py | 6 +- src/user_profile.py | 14 +- src/utils.py | 7 +- 20 files changed, 179 insertions(+), 210 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0a4ea59..a640b312 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,10 +24,6 @@ repos: - id: ruff - id: ruff-format args: [--fix] - - repo: https://github.com/asottile/add-trailing-comma - rev: v3.1.0 # Must match requirements-dev.txt - hooks: - - id: add-trailing-comma ci: autoupdate_branch: dev diff --git a/.vscode/settings.json b/.vscode/settings.json index fcfacc9c..d3e7500a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,14 +25,6 @@ // Let dedicated linter (Ruff) organize imports "source.organizeImports": "never" }, - "emeraldwalk.runonsave": { - "commands": [ - { - "match": "\\.pyi?", - "cmd": "add-trailing-comma ${file}" - }, - ] - }, "files.associations": { ".flake8": "properties", "*.qrc": "xml", diff --git a/ruff.toml b/ruff.toml index 57de7346..109fc6db 100644 --- a/ruff.toml +++ b/ruff.toml @@ -65,8 +65,8 @@ ignore = [ ### # Conflict with formatter (you can remove this section if you don't use Ruff as a formatter) ### - # "COM812", # missing-trailing-comma - # "ISC001", # single-line-implicit-string-concatenation + "COM812", # missing-trailing-comma + "ISC001", # single-line-implicit-string-concatenation ### # Rules about missing special documentation. Up to you if you wanna enable these, you must also disable D406, D407 diff --git a/scripts/lint.ps1 b/scripts/lint.ps1 index ca7c3c57..e5dc4a9c 100644 --- a/scripts/lint.ps1 +++ b/scripts/lint.ps1 @@ -3,11 +3,10 @@ Set-Location "$PSScriptRoot/.." $exitCodes = 0 Write-Host "`nRunning formatting..." -ruff format . -add-trailing-comma $(git ls-files '**.py*') +ruff format Write-Host "`nRunning Ruff ..." -ruff check . --fix +ruff check --fix $exitCodes += $LastExitCode if ($LastExitCode -gt 0) { Write-Host "`Ruff failed ($LastExitCode)" -ForegroundColor Red diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index 21bd4d2a..4d9c2cd4 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -12,7 +12,6 @@ -r requirements.txt # # Linters & Formatters -add-trailing-comma>=3.1.0 # Must match .pre-commit-config.yaml ruff>=0.6.1 # Pre-commit fix # Must match .pre-commit-config.yaml # # Types diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 13789626..a0e0156b 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -11,6 +11,7 @@ # QT doesn't call those from Python/ctypes, meaning we can stop other programs from setting it. if sys.platform == "win32": import ctypes + # pyautogui._pyautogui_win.py ctypes.windll.user32.SetProcessDPIAware = ( # pyright: ignore[reportAttributeAccessIssue] lambda: None @@ -163,8 +164,8 @@ def _show_error_signal_slot(error_message_box: Callable[..., object]): self.setupUi(self) self.setWindowTitle( - f"AutoSplit v{AUTOSPLIT_VERSION}" # fmt: skip - + (" (externally controlled)" if self.is_auto_controlled else ""), + f"AutoSplit v{AUTOSPLIT_VERSION}" + + (" (externally controlled)" if self.is_auto_controlled else "") ) # Hotkeys need to be initialized to be passed as thread arguments in hotkeys.py @@ -211,16 +212,17 @@ def _show_error_signal_slot(error_message_box: Callable[..., object]): self.undo_split_button.clicked.connect(self.undo_split) self.next_image_button.clicked.connect(lambda: self.skip_split(navigate_image_only=True)) self.previous_image_button.clicked.connect( - lambda: self.undo_split(navigate_image_only=True), + lambda: self.undo_split(navigate_image_only=True) ) self.align_region_button.clicked.connect(lambda: align_region(self)) self.select_window_button.clicked.connect(lambda: select_window(self)) self.reload_start_image_button.clicked.connect( - lambda: self.__reload_start_image(started_by_button=True), + lambda: self.__reload_start_image(started_by_button=True) ) self.action_check_for_updates_on_open.changed.connect( lambda: user_profile.set_check_for_updates_on_open( - self, self.action_check_for_updates_on_open.isChecked(), + self, + self.action_check_for_updates_on_open.isChecked(), ), ) @@ -247,7 +249,7 @@ def _update_checker_widget_signal_slot(latest_version: str, check_on_open: bool) # live image checkbox self.timer_live_image.timeout.connect( - lambda: self.__update_live_image_details(None, called_from_timer=True), + lambda: self.__update_live_image_details(None, called_from_timer=True) ) self.timer_live_image.start(int(ONE_SECOND / self.settings_dict["fps_limit"])) @@ -289,7 +291,8 @@ def __browse(self): def __update_live_image_details( self, - capture: MatLike | None, *, + capture: MatLike | None, + *, called_from_timer: bool = False, ): # HACK: Since this is also called in __get_capture_for_comparison, @@ -394,15 +397,12 @@ def __compare_capture_for_auto_start(self): if below_flag and not self.split_below_threshold and similarity_diff >= 0: self.split_below_threshold = True return - if ( - ( # noqa: PLR0916 # See above TODO - below_flag - and self.split_below_threshold - and similarity_diff < 0 - and is_valid_image(capture) - ) - or (not below_flag and similarity_diff >= 0) - ): + if ( # noqa: PLR0916 # See above TODO + below_flag + and self.split_below_threshold + and similarity_diff < 0 + and is_valid_image(capture) + ) or (not below_flag and similarity_diff >= 0): self.timer_start_image.stop() self.split_below_threshold = False @@ -416,7 +416,7 @@ def __compare_capture_for_auto_start(self): while time_delta < start_delay: delay_time_left = start_delay - time_delta self.current_split_image.setText( - f"Delayed Before Starting:\n {seconds_remaining_text(delay_time_left)}", + f"Delayed Before Starting:\n {seconds_remaining_text(delay_time_left)}" ) # Wait 0.1s. Doesn't need to be shorter as we only show 1 decimal QTest.qWait(100) @@ -512,7 +512,7 @@ def __check_fps(self): def __is_current_split_out_of_range(self): return ( - self.split_image_number < 0 # fmt: skip + self.split_image_number < 0 or self.split_image_number > len(self.split_images_and_loop_number) - 1 ) @@ -548,9 +548,7 @@ def skip_split(self, *, navigate_image_only: bool = False): not self.is_running or "Delayed Split" in self.current_split_image.text() or not ( - self.skip_split_button.isEnabled() - or self.is_auto_controlled - or navigate_image_only + self.skip_split_button.isEnabled() or self.is_auto_controlled or navigate_image_only ) or self.__is_current_split_out_of_range() ): @@ -584,9 +582,8 @@ def reset(self): def start_auto_splitter(self): # If the auto splitter is already running or the button is disabled, # don't emit the signal to start it. - if ( - self.is_running # fmt: skip - or (not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled) + if self.is_running or ( + not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled ): return @@ -622,9 +619,9 @@ def __auto_splitter(self): # noqa: C901,PLR0912,PLR0915 # Construct a list of images + loop count tuples. self.split_images_and_loop_number = list( flatten( - ((split_image, i + 1) for i in range(split_image.loops)) # fmt: skip + ((split_image, i + 1) for i in range(split_image.loops)) for split_image in self.split_images - ), + ) ) # Construct groups of splits @@ -726,9 +723,9 @@ def __auto_splitter(self): # noqa: C901,PLR0912,PLR0915 self.gui_changes_on_reset(safe_to_reload_start_image=True) def __similarity_threshold_loop( - self, - number_of_split_images: int, - dummy_splits_array: list[bool], + self, + number_of_split_images: int, + dummy_splits_array: list[bool], ): """ Wait until the similarity threshold is met. @@ -765,7 +762,7 @@ def __similarity_threshold_loop( # If its the last non-dummy split image and last loop number, # disable the skip split button self.skip_split_button.setEnabled( - dummy_splits_array[self.split_image_number :].count(False) > 1, + dummy_splits_array[self.split_image_number :].count(False) > 1 ) self.undo_split_button.setEnabled(self.split_image_number != 0) QApplication.processEvents() @@ -827,7 +824,7 @@ def __pause_loop(self, stop_time: float, message: str): break self.current_split_image.setText( - f"{message} {seconds_remaining_text(stop_time - time_delta)}", + f"{message} {seconds_remaining_text(stop_time - time_delta)}" ) QTest.qWait(1) @@ -903,7 +900,7 @@ def __get_capture_for_comparison(self): message += "\n(captured window may be incompatible with BitBlt)" self.live_image.setText(message) recovered = self.capture_method.recover_window( - self.settings_dict["captured_window_title"], + self.settings_dict["captured_window_title"] ) if recovered: capture = self.capture_method.get_frame() @@ -929,7 +926,7 @@ def __reset_if_should(self, capture: MatLike | None): should_reset = similarity >= threshold self.reset_highest_similarity = max(similarity, self.reset_highest_similarity) self.table_reset_image_highest_label.setText( - decimal(self.reset_highest_similarity), + decimal(self.reset_highest_similarity) ) self.table_reset_image_live_label.setText(decimal(similarity)) @@ -960,8 +957,7 @@ def __update_split_image(self, specific_image: AutoSplitImage | None = None): # Get split image self.split_image = ( - specific_image - or self.split_images_and_loop_number[0 + self.split_image_number][0] + specific_image or self.split_images_and_loop_number[0 + self.split_image_number][0] ) if self.split_image.is_ocr: # TODO: test if setText clears a set image @@ -972,7 +968,7 @@ def __update_split_image(self, specific_image: AutoSplitImage | None = None): self.current_image_file_label.setText(self.split_image.filename) self.table_current_image_threshold_label.setText( - decimal(self.split_image.get_similarity_threshold(self)), + decimal(self.split_image.get_similarity_threshold(self)) ) # Set Image Loop number @@ -1048,19 +1044,13 @@ def set_preview_image(qlabel: QLabel, image: MatLike | None): image_format = QtGui.QImage.Format.Format_BGR888 capture = image - qimage = QtGui.QImage( - capture.data, - width, - height, - width * channels, - image_format, - ) + qimage = QtGui.QImage(capture.data, width, height, width * channels, image_format) qlabel.setPixmap( QtGui.QPixmap(qimage).scaled( qlabel.size(), QtCore.Qt.AspectRatioMode.IgnoreAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation, - ), + ) ) diff --git a/src/AutoSplitImage.py b/src/AutoSplitImage.py index 07701d79..882f21ad 100644 --- a/src/AutoSplitImage.py +++ b/src/AutoSplitImage.py @@ -115,7 +115,7 @@ def __init__(self, path: str): self.__pause_time = pause_from_filename(self.filename) self.__similarity_threshold = threshold_from_filename(self.filename) self.texts: list[str] = [] - self. __ocr_comparison_methods: list[int] = [] + self.__ocr_comparison_methods: list[int] = [] if path.endswith("txt"): self.__parse_text_file(path) else: @@ -193,11 +193,7 @@ def __read_image_bytes(self, path: str): def check_flag(self, flag: int): return self.flags & flag == flag - def compare_with_capture( - self, - default: "AutoSplit | int", - capture: MatLike | None, - ): + def compare_with_capture(self, default: "AutoSplit | int", capture: MatLike | None): """ Compare image with capture using image's comparison method. Falls back to combobox. @@ -210,8 +206,8 @@ def compare_with_capture( if self.is_ocr: return extract_and_compare_text( capture[ - self.__rect[2]:self.__rect[3], - self.__rect[0]:self.__rect[1], + self.__rect[2] : self.__rect[3], + self.__rect[0] : self.__rect[1], ], self.texts, self.__ocr_comparison_methods, diff --git a/src/capture_method/DesktopDuplicationCaptureMethod.py b/src/capture_method/DesktopDuplicationCaptureMethod.py index fac663bb..62930f7d 100644 --- a/src/capture_method/DesktopDuplicationCaptureMethod.py +++ b/src/capture_method/DesktopDuplicationCaptureMethod.py @@ -46,9 +46,7 @@ def get_frame(self): left_bounds, top_bounds, *_ = get_window_bounds(hwnd) self.desktop_duplication.display = next( - display - for display in self.desktop_duplication.displays # fmt: skip - if display.hmonitor == hmonitor + display for display in self.desktop_duplication.displays if display.hmonitor == hmonitor ) offset_x, offset_y, *_ = win32gui.GetWindowRect(hwnd) offset_x -= self.desktop_duplication.display.position["left"] diff --git a/src/capture_method/ScrotCaptureMethod.py b/src/capture_method/ScrotCaptureMethod.py index 7041c7c1..20ed9375 100644 --- a/src/capture_method/ScrotCaptureMethod.py +++ b/src/capture_method/ScrotCaptureMethod.py @@ -19,7 +19,7 @@ class ScrotCaptureMethod(CaptureMethodBase): name = "Scrot" short_description = "very slow, may leave files" description = ( - "\nUses Scrot (SCReenshOT) to take screenshots. " # fmt: skip + "\nUses Scrot (SCReenshOT) to take screenshots. " + "\nLeaves behind a screenshot file if interrupted. " ) diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py index 4d969a39..a2229f14 100644 --- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py +++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py @@ -26,7 +26,7 @@ def is_blank(image: MatLike): :: image.shape[ImageShape.Y] - 1, :: image.shape[ImageShape.X] - 1, ] - == OBS_VIRTUALCAM_PLUGIN_BLANK_PIXEL, + == OBS_VIRTUALCAM_PLUGIN_BLANK_PIXEL ) @@ -34,7 +34,7 @@ class VideoCaptureDeviceCaptureMethod(CaptureMethodBase): name = "Video Capture Device" short_description = "see below" description = ( - "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " # fmt: skip + "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " + "\nYou can select one below. " ) @@ -83,7 +83,7 @@ def __read_loop(self): "AutoSplit encountered an unhandled exception while " + "trying to grab a frame and has stopped capture. " + CREATE_NEW_ISSUE_MESSAGE, - ), + ) ) def __init__(self, autosplit: "AutoSplit"): diff --git a/src/capture_method/WindowsGraphicsCaptureMethod.py b/src/capture_method/WindowsGraphicsCaptureMethod.py index 89a8cbe0..82e29a53 100644 --- a/src/capture_method/WindowsGraphicsCaptureMethod.py +++ b/src/capture_method/WindowsGraphicsCaptureMethod.py @@ -160,8 +160,4 @@ def recover_window(self, captured_window_title: str): @override def check_selected_region_exists(self): - return bool( - is_valid_hwnd(self._autosplit_ref.hwnd) # fmt: skip - and self.frame_pool - and self.session, - ) + return bool(is_valid_hwnd(self._autosplit_ref.hwnd) and self.frame_pool and self.session) diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 8e1c1a7c..e6496c7e 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -125,9 +125,7 @@ def get_method_by_index(self, index: int): # Disallow unsafe get w/o breaking it at runtime @override def __getitem__( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] - self, - key: Never, - /, + self, key: Never, / ) -> type[CaptureMethodBase]: return super().__getitem__(key) @@ -242,7 +240,7 @@ def get_camera_info(index: int, device_name: str): resolution = get_input_device_resolution(index) return ( - CameraInfo(index, device_name, False, backend, resolution) # fmt: skip + CameraInfo(index, device_name, False, backend, resolution) if resolution is not None else None ) diff --git a/src/compare.py b/src/compare.py index f79bce3d..962d0b7d 100644 --- a/src/compare.py +++ b/src/compare.py @@ -55,7 +55,7 @@ def compare_l2_norm(source: MatLike, capture: MatLike, mask: MatLike | None = No # The L2 Error is summed across all pixels, so this normalizes max_error = ( - sqrt(source.size) * MAXBYTE # fmt: skip + sqrt(source.size) * MAXBYTE if not is_valid_image(mask) else sqrt(cv2.countNonZero(mask) * MASK_SIZE_MULTIPLIER) ) @@ -82,9 +82,7 @@ def compare_template(source: MatLike, capture: MatLike, mask: MatLike | None = N # matchTemplate returns the sum of square differences, this is the max # that the value can be. Used for normalizing from 0 to 1. max_error = ( - source.size * MAXBYTE * MAXBYTE # fmt: skip - if not is_valid_image(mask) - else cv2.countNonZero(mask) + source.size * MAXBYTE * MAXBYTE if not is_valid_image(mask) else cv2.countNonZero(mask) ) return 1 - (min_val / max_error) diff --git a/src/error_messages.py b/src/error_messages.py index dae2e4d6..5dac82ca 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -65,14 +65,14 @@ def split_image_directory_empty(): def image_type(image: str): set_text_message( f"{image!r} is not a valid image file, does not exist, " - + "or the full image file path contains a special character.", + + "or the full image file path contains a special character." ) def region(): set_text_message( "No region is selected or the Capture Region window is not open. " - + "Select a region or load settings while the Capture Region window is open.", + + "Select a region or load settings while the Capture Region window is open." ) @@ -83,7 +83,7 @@ def split_hotkey(): def pause_hotkey(): set_text_message( "Your split image folder contains an image filename with a pause flag {p}, " - + "but no pause hotkey is set.", + + "but no pause hotkey is set." ) @@ -97,7 +97,7 @@ def alignment_not_matched(): def no_keyword_image(keyword: str): set_text_message( - f"Your split image folder does not contain an image with the keyword {keyword!r}.", + f"Your split image folder does not contain an image with the keyword {keyword!r}." ) @@ -113,7 +113,7 @@ def old_version_settings_file(): set_text_message( "Old version settings file detected. " + "This version allows settings files in .toml format. " - + "Starting from v2.0.", + + "Starting from v2.0." ) @@ -128,33 +128,33 @@ def invalid_hotkey(hotkey_name: str): def no_settings_file_on_open(): set_text_message( "No settings file found. " - + "One can be loaded on open if placed in the same folder as the AutoSplit executable.", + + "One can be loaded on open if placed in the same folder as the AutoSplit executable." ) def too_many_settings_files_on_open(): set_text_message( "Too many settings files found. " - + "Only one can be loaded on open if placed in the same folder as the AutoSplit executable.", + + "Only one can be loaded on open if placed in the same folder as the AutoSplit executable." ) def check_for_updates(): set_text_message( - "An error occurred while attempting to check for updates. Please check your connection.", + "An error occurred while attempting to check for updates. Please check your connection." ) def load_start_image(): set_text_message( "Start Image found, but cannot be loaded unless Start hotkey is set. " - + "Please set the hotkey, and then click the Reload Start Image button.", + + "Please set the hotkey, and then click the Reload Start Image button." ) def stdin_lost(): set_text_message( - "stdin not supported or lost, external control like LiveSplit integration will not work.", + "stdin not supported or lost, external control like LiveSplit integration will not work." ) @@ -189,7 +189,7 @@ def linux_uinput(): set_text_message( "Failed to create a device file using `uinput` module. " + "This can happen when running Linux under WSL. " - + "Keyboard events have been disabled.", + + "Keyboard events have been disabled." ) @@ -263,7 +263,7 @@ def tesseract_missing(ocr_split_file_path: str): f"{ocr_split_file_path!r} is an Optical Character Recognition split file " + "but tesseract couldn't be found." + f'\nPlease read ' - + f"github.com/{GITHUB_REPOSITORY}#install-tesseract for installation instructions.", + + f"github.com/{GITHUB_REPOSITORY}#install-tesseract for installation instructions." ) @@ -271,5 +271,5 @@ def wrong_ocr_values(ocr_split_file_path: str): set_text_message( f"{ocr_split_file_path!r} has invalid values." + "\nPlease make sure that `left < right` and `top < bottom`. " - + "Also check for negative values in the 'methods' or 'fps_limit' settings", + + "Also check for negative values in the 'methods' or 'fps_limit' settings" ) diff --git a/src/hotkeys.py b/src/hotkeys.py index a3c076e2..44215819 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -31,8 +31,13 @@ Commands = Literal["split", "start", "pause", "reset", "skip", "undo"] Hotkey = Literal[ - "split", "reset", "skip_split", "undo_split", - "pause", "screenshot", "toggle_auto_reset_image", + "split", + "reset", + "skip_split", + "undo_split", + "pause", + "screenshot", + "toggle_auto_reset_image", ] HOTKEYS = ( "split", @@ -116,9 +121,8 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # Deal with regular inputs # If an int or does not contain the following strings - if ( - isinstance(hotkey_or_scan_code, int) # fmt: skip - or not any(key in hotkey_or_scan_code for key in ("num ", "decimal", "+")) + if isinstance(hotkey_or_scan_code, int) or not any( + key in hotkey_or_scan_code for key in ("num ", "decimal", "+") ): keyboard.send(hotkey_or_scan_code) return @@ -128,12 +132,9 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # Even by sending specific scan code "keyboard" still sends the default (wrong) key # keyboard also has issues with capitalization modifier (shift+A) # keyboard.send(keyboard.key_to_scan_codes(key_or_scan_code)[1]) - pyautogui.hotkey( - *[ - "+" if key == "plus" else key # fmt: skip - for key in hotkey_or_scan_code.replace(" ", "").split("+") - ], - ) + pyautogui.hotkey(*[ + "+" if key == "plus" else key for key in hotkey_or_scan_code.replace(" ", "").split("+") + ]) def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) -> bool: @@ -157,9 +158,9 @@ def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) if keyboard_event.name and is_digit(keyboard_event.name[-1]): # Prevent "regular numbers" and "keypad numbers" from activating each other return bool( - keyboard_event.is_keypad # fmt: skip + keyboard_event.is_keypad if expected_key.startswith("num ") - else not keyboard_event.is_keypad, + else not keyboard_event.is_keypad ) # Prevent "keypad action keys" from triggering "regular numbers" and "keypad numbers" @@ -169,7 +170,8 @@ def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) def _hotkey_action( keyboard_event: keyboard.KeyboardEvent, - key_name: str, action: Callable[[], None], + key_name: str, + action: Callable[[], None], ): """ We're doing the check here instead of saving the key code because @@ -177,7 +179,8 @@ def _hotkey_action( They also share scan codes on Windows. """ if keyboard_event.event_type == keyboard.KEY_DOWN and __validate_keypad( - key_name, keyboard_event, + key_name, + keyboard_event, ): action() @@ -189,7 +192,7 @@ def __get_key_name(keyboard_event: keyboard.KeyboardEvent): if event_name == "+": return "plus" return ( - f"num {keyboard_event.name}" # fmt: skip + f"num {keyboard_event.name}" if keyboard_event.is_keypad and is_digit(keyboard_event.name) else event_name ) @@ -272,7 +275,7 @@ def toggle_auto_reset_image(): def is_valid_hotkey_name(hotkey_name: str): return any( - key and not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) # fmt: skip + key and not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) for key in hotkey_name.split("+") ) diff --git a/src/menu_bar.py b/src/menu_bar.py index 9c29f9ba..df78d02d 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -31,12 +31,16 @@ HALF_BRIGHTNESS = 128 LINUX_SCREENSHOT_SUPPORT = ( - "\n\n----------------------------------------------------\n\n" - + error_messages.WAYLAND_WARNING - # Keep in sync with README.md#Capture_Method_Linux - + '\n"scrot" must be installed to use SCReenshOT. ' - + "\nRun: sudo apt-get install scrot" -) if sys.platform == "linux" else "" + ( + "\n\n----------------------------------------------------\n\n" + + error_messages.WAYLAND_WARNING + # Keep in sync with README.md#Capture_Method_Linux + + '\n"scrot" must be installed to use SCReenshOT. ' + + "\nRun: sudo apt-get install scrot" + ) + if sys.platform == "linux" + else "" +) class __AboutWidget(QtWidgets.QWidget, about.Ui_AboutAutoSplitWidget): # noqa: N801 # Private class @@ -58,11 +62,11 @@ def open_about(autosplit: "AutoSplit"): class __UpdateCheckerWidget(QtWidgets.QWidget, update_checker.Ui_UpdateChecker): # noqa: N801 # Private class def __init__( - self, - latest_version: str, - design_window: design.Ui_MainWindow, - *, - check_on_open: bool = False, + self, + latest_version: str, + design_window: design.Ui_MainWindow, + *, + check_on_open: bool = False, ): super().__init__() self.setupUi(self) @@ -95,12 +99,14 @@ def do_not_ask_me_again_state_changed(self): def open_update_checker(autosplit: "AutoSplit", latest_version: str, *, check_on_open: bool): - if not autosplit.UpdateCheckerWidget or cast( - QtWidgets.QWidget, - autosplit.UpdateCheckerWidget, - ).isHidden(): + if ( + not autosplit.UpdateCheckerWidget + or cast(QtWidgets.QWidget, autosplit.UpdateCheckerWidget).isHidden() + ): autosplit.UpdateCheckerWidget = __UpdateCheckerWidget( - latest_version, autosplit, check_on_open=check_on_open, + latest_version, + autosplit, + check_on_open=check_on_open, ) @@ -118,12 +124,14 @@ def __init__(self, autosplit: "AutoSplit", *, check_on_open: bool): def run(self): try: with urlopen( - f"https://api.github.com/repos/{GITHUB_REPOSITORY}/releases/latest", timeout=30, + f"https://api.github.com/repos/{GITHUB_REPOSITORY}/releases/latest", + timeout=30, ) as response: json_response: dict[str, str] = json.loads(response.read()) latest_version = json_response["name"].split("v")[1] self._autosplit_ref.update_checker_widget_signal.emit( - latest_version, self.check_on_open, + latest_version, + self.check_on_open, ) except (URLError, KeyError): if not self.check_on_open: @@ -140,7 +148,8 @@ def about_qt_for_python(): def check_for_updates(autosplit: "AutoSplit", *, check_on_open: bool = False): autosplit.CheckForUpdatesThread = __CheckForUpdatesThread( - autosplit, check_on_open=check_on_open, + autosplit, + check_on_open=check_on_open, ) autosplit.CheckForUpdatesThread.start() @@ -173,7 +182,7 @@ def __init__(self, autosplit: "AutoSplit"): # Don't autofocus any particular field self.setFocus() -# region Build the Capture method combobox + # region Build the Capture method combobox capture_method_values = CAPTURE_METHODS.values() self.__set_all_capture_devices() @@ -187,16 +196,15 @@ def __init__(self, autosplit: "AutoSplit"): # self.capture_method_combobox.setView(list_view) self.capture_method_combobox.addItems([ - f"- {method.name} ({method.short_description})" - for method in capture_method_values + f"- {method.name} ({method.short_description})" for method in capture_method_values ]) self.capture_method_combobox.setToolTip( "\n\n".join([ - f"{method.name} :\n{method.description}" - for method in capture_method_values - ]) + LINUX_SCREENSHOT_SUPPORT, + f"{method.name} :\n{method.description}" for method in capture_method_values + ]) + + LINUX_SCREENSHOT_SUPPORT ) -# endregion + # endregion self.__setup_bindings() @@ -207,12 +215,12 @@ def __update_default_threshold(self, value: Any): self._autosplit_ref.table_current_image_threshold_label.setText( decimal(self._autosplit_ref.split_image.get_similarity_threshold(self._autosplit_ref)) if self._autosplit_ref.split_image - else "-", + else "-" ) self._autosplit_ref.table_reset_image_threshold_label.setText( decimal(self._autosplit_ref.reset_image.get_similarity_threshold(self._autosplit_ref)) if self._autosplit_ref.reset_image - else "-", + else "-" ) def __set_value(self, key: str, value: Any): @@ -221,10 +229,9 @@ def __set_value(self, key: str, value: Any): def get_capture_device_index(self, capture_device_id: int): """Returns 0 if the capture_device_id is invalid.""" try: - return [ - device.device_id for device - in self.__video_capture_devices - ].index(capture_device_id) + return [device.device_id for device in self.__video_capture_devices].index( + capture_device_id + ) except ValueError: return 0 @@ -239,8 +246,8 @@ def __enable_capture_device_if_its_selected_method( if is_video_capture_device: self.capture_device_combobox.setCurrentIndex( self.get_capture_device_index( - self._autosplit_ref.settings_dict["capture_device_id"], - ), + self._autosplit_ref.settings_dict["capture_device_id"] + ) ) else: self.capture_device_combobox.setPlaceholderText('Select "Video Capture Device" above') @@ -248,7 +255,7 @@ def __enable_capture_device_if_its_selected_method( def __capture_method_changed(self): selected_capture_method = CAPTURE_METHODS.get_method_by_index( - self.capture_method_combobox.currentIndex(), + self.capture_method_combobox.currentIndex() ) self.__enable_capture_device_if_its_selected_method(selected_capture_method) change_capture_method(selected_capture_method, self._autosplit_ref) @@ -291,14 +298,12 @@ def __set_all_capture_devices(self): def __set_readme_link(self): self.custom_image_settings_info_label.setText( - self.custom_image_settings_info_label - .text() - .format(GITHUB_REPOSITORY=GITHUB_REPOSITORY), + self.custom_image_settings_info_label.text().format(GITHUB_REPOSITORY=GITHUB_REPOSITORY) ) # HACK: This is a workaround because custom_image_settings_info_label # simply will not open links with a left click no matter what we tried. self.readme_link_button.clicked.connect( - lambda: webbrowser.open(f"https://github.com/{GITHUB_REPOSITORY}#readme"), + lambda: webbrowser.open(f"https://github.com/{GITHUB_REPOSITORY}#readme") ) self.readme_link_button.setStyleSheet("border: 0px; background-color:rgba(0,0,0,0%);") if sys.platform == "linux": @@ -306,7 +311,7 @@ def __set_readme_link(self): # In-button font has different width so "README" doesn't fit -.- self.readme_link_button.setText("#DOC#") self.readme_link_button.setGeometry( - QtCore.QRect(116, 220, geometry.width(), geometry.height()), + QtCore.QRect(116, 220, geometry.width(), geometry.height()) ) def __select_screenshot_directory(self): @@ -319,7 +324,7 @@ def __select_screenshot_directory(self): ) ) self.screenshot_directory_input.setText( - self._autosplit_ref.settings_dict["screenshot_directory"], + self._autosplit_ref.settings_dict["screenshot_directory"] ) def __setup_bindings(self): @@ -327,7 +332,8 @@ def __setup_bindings(self): for hotkey in HOTKEYS: hotkey_input: QtWidgets.QLineEdit = getattr(self, f"{hotkey}_input") set_hotkey_hotkey_button: QtWidgets.QPushButton = getattr( - self, f"set_{hotkey}_hotkey_button", + self, + f"set_{hotkey}_hotkey_button", ) hotkey_input.setText(self._autosplit_ref.settings_dict.get(f"{hotkey}_hotkey", "")) @@ -337,63 +343,64 @@ def __setup_bindings(self): hotkey_input.setEnabled(False) else: set_hotkey_hotkey_button.clicked.connect( - partial(set_hotkey, self._autosplit_ref, hotkey=hotkey), + partial(set_hotkey, self._autosplit_ref, hotkey=hotkey) ) -# region Set initial values + # region Set initial values # Capture Settings self.fps_limit_spinbox.setValue(self._autosplit_ref.settings_dict["fps_limit"]) self.live_capture_region_checkbox.setChecked( - self._autosplit_ref.settings_dict["live_capture_region"], + self._autosplit_ref.settings_dict["live_capture_region"] ) self.capture_method_combobox.setCurrentIndex( - CAPTURE_METHODS.get_index(self._autosplit_ref.settings_dict["capture_method"]), + CAPTURE_METHODS.get_index(self._autosplit_ref.settings_dict["capture_method"]) ) # No self.capture_device_combobox.setCurrentIndex # It'll set itself asynchronously in self.__set_all_capture_devices() self.screenshot_directory_input.setText( - self._autosplit_ref.settings_dict["screenshot_directory"], + self._autosplit_ref.settings_dict["screenshot_directory"] ) self.open_screenshot_checkbox.setChecked( - self._autosplit_ref.settings_dict["open_screenshot"], + self._autosplit_ref.settings_dict["open_screenshot"] ) # Image Settings self.default_comparison_method_combobox.setCurrentIndex( - self._autosplit_ref.settings_dict["default_comparison_method"], + self._autosplit_ref.settings_dict["default_comparison_method"] ) self.default_similarity_threshold_spinbox.setValue( - self._autosplit_ref.settings_dict["default_similarity_threshold"], + self._autosplit_ref.settings_dict["default_similarity_threshold"] ) self.default_delay_time_spinbox.setValue( - self._autosplit_ref.settings_dict["default_delay_time"], + self._autosplit_ref.settings_dict["default_delay_time"] ) self.default_pause_time_spinbox.setValue( - self._autosplit_ref.settings_dict["default_pause_time"], + self._autosplit_ref.settings_dict["default_pause_time"] ) self.loop_splits_checkbox.setChecked(self._autosplit_ref.settings_dict["loop_splits"]) self.start_also_resets_checkbox.setChecked( - self._autosplit_ref.settings_dict["start_also_resets"], + self._autosplit_ref.settings_dict["start_also_resets"] ) self.enable_auto_reset_image_checkbox.setChecked( - self._autosplit_ref.settings_dict["enable_auto_reset"], + self._autosplit_ref.settings_dict["enable_auto_reset"] ) -# endregion -# region Binding + # endregion + # region Binding # Capture Settings self.fps_limit_spinbox.valueChanged.connect(self.__fps_limit_changed) self.live_capture_region_checkbox.stateChanged.connect( lambda: self.__set_value( - "live_capture_region", self.live_capture_region_checkbox.isChecked(), - ), + "live_capture_region", + self.live_capture_region_checkbox.isChecked(), + ) ) self.capture_method_combobox.currentIndexChanged.connect( - lambda: self.__set_value("capture_method", self.__capture_method_changed()), + lambda: self.__set_value("capture_method", self.__capture_method_changed()) ) self.capture_device_combobox.currentIndexChanged.connect(self.__capture_device_changed) self.screenshot_directory_browse_button.clicked.connect(self.__select_screenshot_directory) self.open_screenshot_checkbox.stateChanged.connect( - lambda: self.__set_value("open_screenshot", self.open_screenshot_checkbox.isChecked()), + lambda: self.__set_value("open_screenshot", self.open_screenshot_checkbox.isChecked()) ) # Image Settings @@ -401,32 +408,36 @@ def __setup_bindings(self): lambda: self.__set_value( "default_comparison_method", self.default_comparison_method_combobox.currentIndex(), - ), + ) ) self.default_similarity_threshold_spinbox.valueChanged.connect( lambda: self.__update_default_threshold( - self.default_similarity_threshold_spinbox.value(), - ), + self.default_similarity_threshold_spinbox.value() + ) ) self.default_delay_time_spinbox.valueChanged.connect( - lambda: self.__set_value("default_delay_time", self.default_delay_time_spinbox.value()), + lambda: self.__set_value("default_delay_time", self.default_delay_time_spinbox.value()) ) self.default_pause_time_spinbox.valueChanged.connect( - lambda: self.__set_value("default_pause_time", self.default_pause_time_spinbox.value()), + lambda: self.__set_value("default_pause_time", self.default_pause_time_spinbox.value()) ) self.loop_splits_checkbox.stateChanged.connect( - lambda: self.__set_value("loop_splits", self.loop_splits_checkbox.isChecked()), + lambda: self.__set_value("loop_splits", self.loop_splits_checkbox.isChecked()) ) self.start_also_resets_checkbox.stateChanged.connect( lambda: self.__set_value( - "start_also_resets", self.start_also_resets_checkbox.isChecked(), - ), + "start_also_resets", + self.start_also_resets_checkbox.isChecked(), + ) ) self.enable_auto_reset_image_checkbox.stateChanged.connect( lambda: self.__set_value( - "enable_auto_reset", self.enable_auto_reset_image_checkbox.isChecked(), - ), + "enable_auto_reset", + self.enable_auto_reset_image_checkbox.isChecked(), + ) ) + + # endregion @@ -452,7 +463,7 @@ def get_default_settings_from_ui(autosplit: "AutoSplit"): "fps_limit": default_settings_dialog.fps_limit_spinbox.value(), "live_capture_region": default_settings_dialog.live_capture_region_checkbox.isChecked(), "capture_method": CAPTURE_METHODS.get_method_by_index( - default_settings_dialog.capture_method_combobox.currentIndex(), + default_settings_dialog.capture_method_combobox.currentIndex() ), "capture_device_id": default_settings_dialog.capture_device_combobox.currentIndex(), "capture_device_name": "", diff --git a/src/region_selection.py b/src/region_selection.py index 10e44b7e..325aca61 100644 --- a/src/region_selection.py +++ b/src/region_selection.py @@ -65,9 +65,7 @@ + " ".join([f"{extensions}" for _, extensions in SUPPORTED_IMREAD_FORMATS]) + ");;" + ";;".join([ - f"{imread_format} ({extensions})" - for imread_format, extensions - in SUPPORTED_IMREAD_FORMATS + f"{imread_format} ({extensions})" for imread_format, extensions in SUPPORTED_IMREAD_FORMATS ]) ) diff --git a/src/split_parser.py b/src/split_parser.py index 069ad286..af0a3dea 100644 --- a/src/split_parser.py +++ b/src/split_parser.py @@ -196,7 +196,7 @@ def validate_before_parsing(autosplit: "AutoSplit", *, show_error: bool = True): def parse_and_validate_images(autosplit: "AutoSplit"): # Get split images all_images = [ - AutoSplitImage(os.path.join(autosplit.settings_dict["split_image_directory"], image_name)) # fmt: skip + AutoSplitImage(os.path.join(autosplit.settings_dict["split_image_directory"], image_name)) for image_name in os.listdir(autosplit.settings_dict["split_image_directory"]) ] @@ -210,7 +210,7 @@ def parse_and_validate_images(autosplit: "AutoSplit"): # If there is no start hotkey set but a Start Image is present, # and is not auto controlled, throw an error. if ( - start_image # fmt: skip + start_image and not autosplit.settings_dict["split_hotkey"] and not autosplit.is_auto_controlled ): @@ -219,7 +219,7 @@ def parse_and_validate_images(autosplit: "AutoSplit"): # If there is no reset hotkey set but a Reset Image is present, # and is not auto controlled, throw an error. elif ( - reset_image # fmt: skip + reset_image and not autosplit.settings_dict["reset_hotkey"] and not autosplit.is_auto_controlled ): diff --git a/src/user_profile.py b/src/user_profile.py index f97cef74..ad028624 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -96,7 +96,8 @@ def save_settings_as(autosplit: "AutoSplit"): save_settings_file_path = QtWidgets.QFileDialog.getSaveFileName( autosplit, "Save Settings As", - autosplit.last_successfully_loaded_settings_file_path or os.path.join(auto_split_directory, "settings.toml"), + autosplit.last_successfully_loaded_settings_file_path + or os.path.join(auto_split_directory, "settings.toml"), "TOML (*.toml)", )[0] @@ -166,7 +167,7 @@ def __load_settings_from_file(autosplit: "AutoSplit", load_settings_file_path: s autosplit.live_image.setText( "Reload settings after opening" + f"\n{autosplit.settings_dict['captured_window_title']!r}" - + "\nto automatically load Capture Region", + + "\nto automatically load Capture Region" ) if settings_widget_was_open: @@ -186,8 +187,7 @@ def load_settings(autosplit: "AutoSplit", from_path: str = ""): )[0] ) if not ( - load_settings_file_path - and __load_settings_from_file(autosplit, load_settings_file_path) + load_settings_file_path and __load_settings_from_file(autosplit, load_settings_file_path) ): return @@ -198,11 +198,7 @@ def load_settings(autosplit: "AutoSplit", from_path: str = ""): def load_settings_on_open(autosplit: "AutoSplit"): - settings_files = [ - file # fmt: skip - for file in os.listdir(auto_split_directory) - if file.endswith(".toml") - ] + settings_files = [file for file in os.listdir(auto_split_directory) if file.endswith(".toml")] # Find all .tomls in AutoSplit folder, error if there is not exactly 1 error = None diff --git a/src/utils.py b/src/utils.py index d86bc5e7..0264128c 100644 --- a/src/utils.py +++ b/src/utils.py @@ -220,7 +220,7 @@ async def init_mediacapture(): # May be problematic? # https://github.com/pywinrt/python-winsdk/issues/11#issuecomment-1315345318 direct_3d_device = LearningModelDevice( - LearningModelDeviceKind.DIRECT_X_HIGH_PERFORMANCE, + LearningModelDeviceKind.DIRECT_X_HIGH_PERFORMANCE ).direct3_d11_device # TODO: Unknown potential error, I don't have an older Win10 machine to test. except BaseException: # noqa: S110,BLE001 @@ -294,7 +294,7 @@ def subprocess_kwargs(): Typical use: ```python - subprocess.call(['program_to_run', 'arg_1'], **subprocess_args()) + subprocess.call(["program_to_run", "arg_1"], **subprocess_args()) ``` --- Originally found in https://github.com/madmaze/pytesseract/blob/master/pytesseract/pytesseract.py @@ -334,8 +334,7 @@ def run_tesseract(png: bytes): @return: The recognized output string from tesseract. """ return ( - subprocess # noqa: S603 # Only using known literal strings - .Popen(TESSERACT_CMD, **subprocess_kwargs()) + subprocess.Popen(TESSERACT_CMD, **subprocess_kwargs()) # noqa: S603 # Only using known literal strings .communicate(input=png)[0] .decode() ) From 1750b3a0f9b3caa33b87e61289ce940168d22715 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 18 Aug 2024 15:48:28 -0400 Subject: [PATCH 6/6] Update preferences --- .pre-commit-config.yaml | 2 +- .vscode/extensions.json | 1 - ruff.toml | 3 +- src/AutoSplit.py | 12 ++++--- .../DesktopDuplicationCaptureMethod.py | 4 ++- .../WindowsGraphicsCaptureMethod.py | 6 +++- src/compare.py | 4 ++- src/hotkeys.py | 8 +++-- src/menu_bar.py | 35 +++++++++---------- src/region_selection.py | 6 ++-- src/user_profile.py | 9 +++-- src/utils.py | 4 ++- 12 files changed, 56 insertions(+), 38 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a640b312..bc37adec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,8 +22,8 @@ repos: rev: v0.6.1 # Must match requirements-dev.txt hooks: - id: ruff - - id: ruff-format args: [--fix] + - id: ruff-format ci: autoupdate_branch: dev diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a7722a2b..ecd26dfc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,7 +4,6 @@ "charliermarsh.ruff", "davidanson.vscode-markdownlint", "eamodio.gitlens", - "emeraldwalk.runonsave", "github.vscode-github-actions", "ms-python.python", "ms-python.vscode-pylance", diff --git a/ruff.toml b/ruff.toml index 109fc6db..79adc33c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -67,6 +67,7 @@ ignore = [ ### "COM812", # missing-trailing-comma "ISC001", # single-line-implicit-string-concatenation + "RUF028", # invalid-formatter-suppression-comment, Is meant for the formatter, but false-positives ### # Rules about missing special documentation. Up to you if you wanna enable these, you must also disable D406, D407 @@ -80,7 +81,7 @@ ignore = [ ### # Specific to this project ### - "D205", # Not all docstrings have a short description + desrciption + "D205", # Not all docstrings have a short description + description # TODO: Consider for more complete doc "DOC201", # docstring-extraneous-returns "DOC501", # docstring-missing-exception diff --git a/src/AutoSplit.py b/src/AutoSplit.py index a0e0156b..3e315ac3 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -548,7 +548,9 @@ def skip_split(self, *, navigate_image_only: bool = False): not self.is_running or "Delayed Split" in self.current_split_image.text() or not ( - self.skip_split_button.isEnabled() or self.is_auto_controlled or navigate_image_only + self.skip_split_button.isEnabled() # fmt: skip + or self.is_auto_controlled + or navigate_image_only ) or self.__is_current_split_out_of_range() ): @@ -582,8 +584,9 @@ def reset(self): def start_auto_splitter(self): # If the auto splitter is already running or the button is disabled, # don't emit the signal to start it. - if self.is_running or ( - not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled + if ( # fmt: skip + self.is_running + or (not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled) ): return @@ -957,7 +960,8 @@ def __update_split_image(self, specific_image: AutoSplitImage | None = None): # Get split image self.split_image = ( - specific_image or self.split_images_and_loop_number[0 + self.split_image_number][0] + specific_image # fmt: skip + or self.split_images_and_loop_number[0 + self.split_image_number][0] ) if self.split_image.is_ocr: # TODO: test if setText clears a set image diff --git a/src/capture_method/DesktopDuplicationCaptureMethod.py b/src/capture_method/DesktopDuplicationCaptureMethod.py index 62930f7d..827f443c 100644 --- a/src/capture_method/DesktopDuplicationCaptureMethod.py +++ b/src/capture_method/DesktopDuplicationCaptureMethod.py @@ -46,7 +46,9 @@ def get_frame(self): left_bounds, top_bounds, *_ = get_window_bounds(hwnd) self.desktop_duplication.display = next( - display for display in self.desktop_duplication.displays if display.hmonitor == hmonitor + display + for display in self.desktop_duplication.displays + if display.hmonitor == hmonitor # fmt: skip ) offset_x, offset_y, *_ = win32gui.GetWindowRect(hwnd) offset_x -= self.desktop_duplication.display.position["left"] diff --git a/src/capture_method/WindowsGraphicsCaptureMethod.py b/src/capture_method/WindowsGraphicsCaptureMethod.py index 82e29a53..dd632b66 100644 --- a/src/capture_method/WindowsGraphicsCaptureMethod.py +++ b/src/capture_method/WindowsGraphicsCaptureMethod.py @@ -160,4 +160,8 @@ def recover_window(self, captured_window_title: str): @override def check_selected_region_exists(self): - return bool(is_valid_hwnd(self._autosplit_ref.hwnd) and self.frame_pool and self.session) + return bool( + is_valid_hwnd(self._autosplit_ref.hwnd) # fmt: skip + and self.frame_pool + and self.session + ) diff --git a/src/compare.py b/src/compare.py index 962d0b7d..868293cd 100644 --- a/src/compare.py +++ b/src/compare.py @@ -82,7 +82,9 @@ def compare_template(source: MatLike, capture: MatLike, mask: MatLike | None = N # matchTemplate returns the sum of square differences, this is the max # that the value can be. Used for normalizing from 0 to 1. max_error = ( - source.size * MAXBYTE * MAXBYTE if not is_valid_image(mask) else cv2.countNonZero(mask) + source.size * MAXBYTE * MAXBYTE # fmt: skip + if not is_valid_image(mask) + else cv2.countNonZero(mask) ) return 1 - (min_val / max_error) diff --git a/src/hotkeys.py b/src/hotkeys.py index 44215819..cf433662 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -121,8 +121,9 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # Deal with regular inputs # If an int or does not contain the following strings - if isinstance(hotkey_or_scan_code, int) or not any( - key in hotkey_or_scan_code for key in ("num ", "decimal", "+") + if ( # fmt: skip + isinstance(hotkey_or_scan_code, int) + or not any(key in hotkey_or_scan_code for key in ("num ", "decimal", "+")) ): keyboard.send(hotkey_or_scan_code) return @@ -133,7 +134,8 @@ def _send_hotkey(hotkey_or_scan_code: int | str | None): # keyboard also has issues with capitalization modifier (shift+A) # keyboard.send(keyboard.key_to_scan_codes(key_or_scan_code)[1]) pyautogui.hotkey(*[ - "+" if key == "plus" else key for key in hotkey_or_scan_code.replace(" ", "").split("+") + "+" if key == "plus" else key # fmt: skip + for key in hotkey_or_scan_code.replace(" ", "").split("+") ]) diff --git a/src/menu_bar.py b/src/menu_bar.py index df78d02d..4c3e9f06 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -31,16 +31,12 @@ HALF_BRIGHTNESS = 128 LINUX_SCREENSHOT_SUPPORT = ( - ( - "\n\n----------------------------------------------------\n\n" - + error_messages.WAYLAND_WARNING - # Keep in sync with README.md#Capture_Method_Linux - + '\n"scrot" must be installed to use SCReenshOT. ' - + "\nRun: sudo apt-get install scrot" - ) - if sys.platform == "linux" - else "" -) + "\n\n----------------------------------------------------\n\n" + + error_messages.WAYLAND_WARNING + # Keep in sync with README.md#Capture_Method_Linux + + '\n"scrot" must be installed to use SCReenshOT. ' + + "\nRun: sudo apt-get install scrot" +) if sys.platform == "linux" else "" # fmt: skip class __AboutWidget(QtWidgets.QWidget, about.Ui_AboutAutoSplitWidget): # noqa: N801 # Private class @@ -182,7 +178,8 @@ def __init__(self, autosplit: "AutoSplit"): # Don't autofocus any particular field self.setFocus() - # region Build the Capture method combobox + # region Build the Capture method combobox # fmt: skip + capture_method_values = CAPTURE_METHODS.values() self.__set_all_capture_devices() @@ -199,9 +196,9 @@ def __init__(self, autosplit: "AutoSplit"): f"- {method.name} ({method.short_description})" for method in capture_method_values ]) self.capture_method_combobox.setToolTip( - "\n\n".join([ + "\n\n".join( f"{method.name} :\n{method.description}" for method in capture_method_values - ]) + ) + LINUX_SCREENSHOT_SUPPORT ) # endregion @@ -229,9 +226,10 @@ def __set_value(self, key: str, value: Any): def get_capture_device_index(self, capture_device_id: int): """Returns 0 if the capture_device_id is invalid.""" try: - return [device.device_id for device in self.__video_capture_devices].index( - capture_device_id - ) + return [ + device.device_id # fmt: skip + for device in self.__video_capture_devices + ].index(capture_device_id) except ValueError: return 0 @@ -385,6 +383,7 @@ def __setup_bindings(self): self._autosplit_ref.settings_dict["enable_auto_reset"] ) # endregion + # region Binding # Capture Settings self.fps_limit_spinbox.valueChanged.connect(self.__fps_limit_changed) @@ -436,9 +435,7 @@ def __setup_bindings(self): self.enable_auto_reset_image_checkbox.isChecked(), ) ) - - -# endregion + # endregion def open_settings(autosplit: "AutoSplit"): diff --git a/src/region_selection.py b/src/region_selection.py index 325aca61..cbed6298 100644 --- a/src/region_selection.py +++ b/src/region_selection.py @@ -62,11 +62,11 @@ """https://docs.opencv.org/4.8.0/d4/da8/group__imgcodecs.html#imread""" IMREAD_EXT_FILTER = ( "All Files (" - + " ".join([f"{extensions}" for _, extensions in SUPPORTED_IMREAD_FORMATS]) + + " ".join(f"{extensions}" for _, extensions in SUPPORTED_IMREAD_FORMATS) + ");;" - + ";;".join([ + + ";;".join( f"{imread_format} ({extensions})" for imread_format, extensions in SUPPORTED_IMREAD_FORMATS - ]) + ) ) diff --git a/src/user_profile.py b/src/user_profile.py index ad028624..723af2a1 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -187,7 +187,8 @@ def load_settings(autosplit: "AutoSplit", from_path: str = ""): )[0] ) if not ( - load_settings_file_path and __load_settings_from_file(autosplit, load_settings_file_path) + load_settings_file_path # fmt: skip + and __load_settings_from_file(autosplit, load_settings_file_path) ): return @@ -198,7 +199,11 @@ def load_settings(autosplit: "AutoSplit", from_path: str = ""): def load_settings_on_open(autosplit: "AutoSplit"): - settings_files = [file for file in os.listdir(auto_split_directory) if file.endswith(".toml")] + settings_files = [ + file # fmt: skip + for file in os.listdir(auto_split_directory) + if file.endswith(".toml") + ] # Find all .tomls in AutoSplit folder, error if there is not exactly 1 error = None diff --git a/src/utils.py b/src/utils.py index 0264128c..de56f021 100644 --- a/src/utils.py +++ b/src/utils.py @@ -334,7 +334,9 @@ def run_tesseract(png: bytes): @return: The recognized output string from tesseract. """ return ( - subprocess.Popen(TESSERACT_CMD, **subprocess_kwargs()) # noqa: S603 # Only using known literal strings + subprocess.Popen( # noqa: S603 # Only using known literal strings + TESSERACT_CMD, **subprocess_kwargs() + ) .communicate(input=png)[0] .decode() )