diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml index 85f116ab..1089c845 100644 --- a/.github/workflows/lint-and-build.yml +++ b/.github/workflows/lint-and-build.yml @@ -89,6 +89,7 @@ jobs: - name: Analysing the code with Pyright uses: jakebailey/pyright-action@v1 with: + version: "1.1.364" working-directory: src/ python-version: ${{ matrix.python-version }} Build: diff --git a/.vscode/settings.json b/.vscode/settings.json index 815e9679..138878f4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,6 +62,10 @@ "[json][jsonc]": { "editor.defaultFormatter": "vscode.json-language-features", }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "yaml.format.printWidth": 100, "[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", diff --git a/pyproject.toml b/pyproject.toml index 41b4e3a5..bb3a68c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ ignore = [ "ERA001", # eradicate: commented-out-code # contextlib.suppress is roughly 3x slower than try/except "SIM105", # flake8-simplify: use-contextlib-suppress - # Negative performance impact + # Slower and more verbose https://github.com/astral-sh/ruff/issues/7871 "UP038", # non-pep604-isinstance # Checked by type-checker (pyright) "ANN", # flake-annotations diff --git a/scripts/lint.ps1 b/scripts/lint.ps1 index 84dc6f8b..3e0eacb1 100644 --- a/scripts/lint.ps1 +++ b/scripts/lint.ps1 @@ -6,7 +6,7 @@ Write-Host "`nRunning formatting..." autopep8 src/ --recursive --in-place add-trailing-comma $(git ls-files '**.py*') -Write-Host "`nRunning Ruff..." +Write-Host "`nRunning Ruff ..." ruff check . --fix $exitCodes += $LastExitCode if ($LastExitCode -gt 0) { @@ -16,12 +16,16 @@ else { Write-Host "`Ruff passed" -ForegroundColor Green } -Write-Host "`nRunning Pyright..." -$Env:PYRIGHT_PYTHON_FORCE_VERSION = 'latest' -npx pyright@latest src/ +$pyrightVersion = '1.1.364' # Change this if latest has issues +Write-Host "`nRunning Pyright $pyrightVersion ..." +$Env:PYRIGHT_PYTHON_FORCE_VERSION = $pyrightVersion +npx -y pyright@$pyrightVersion src/ $exitCodes += $LastExitCode if ($LastExitCode -gt 0) { Write-Host "`Pyright failed ($LastExitCode)" -ForegroundColor Red + if ($pyrightVersion -eq 'latest') { + npx pyright@latest --version + } } else { Write-Host "`Pyright passed" -ForegroundColor Green diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 9314101a..20bbfab0 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -6,7 +6,7 @@ from copy import deepcopy from time import time from types import FunctionType -from typing import NoReturn +from typing import NoReturn, cast import cv2 from cv2.typing import MatLike @@ -946,7 +946,8 @@ def set_preview_image(qlabel: QLabel, image: MatLike | None): capture = image qimage = QtGui.QImage( - capture.data, + # Try to update PySide6, see https://bugreports.qt.io/browse/QTBUG-114635 + cast(bytes, capture.data) if sys.platform == "linux" else capture.data, width, height, width * channels, diff --git a/src/capture_method/Screenshot using QT attempt.py b/src/capture_method/Screenshot using QT attempt.py index fa55e8d5..abb3d3af 100644 --- a/src/capture_method/Screenshot using QT attempt.py +++ b/src/capture_method/Screenshot using QT attempt.py @@ -1,17 +1,18 @@ -# flake8: noqa +# ruff: noqa: RET504 import sys if sys.platform != "linux": - raise OSError() + raise OSError from typing import cast import numpy as np from cv2.typing import MatLike from PySide6.QtCore import QBuffer, QIODeviceBase from PySide6.QtGui import QGuiApplication -from capture_method.CaptureMethodBase import CaptureMethodBase from typing_extensions import override +from capture_method.CaptureMethodBase import CaptureMethodBase + class QtCaptureMethod(CaptureMethodBase): _render_full_content = False diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py index fc62e1fc..29606f95 100644 --- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py +++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py @@ -1,4 +1,3 @@ -import sys from threading import Event, Thread from typing import TYPE_CHECKING @@ -8,13 +7,11 @@ from cv2.typing import MatLike from typing_extensions import override +from capture_method import get_input_device_resolution from capture_method.CaptureMethodBase import CaptureMethodBase from error_messages import CREATE_NEW_ISSUE_MESSAGE, exception_traceback from utils import ImageShape, is_valid_image -if sys.platform == "win32": - from pygrabber.dshow_graph import FilterGraph - if TYPE_CHECKING: from AutoSplit import AutoSplit @@ -101,14 +98,11 @@ def __init__(self, autosplit: "AutoSplit"): return # Ensure we're using the right camera size. And not OpenCV's default 640x480 - if sys.platform == "win32": - filter_graph = FilterGraph() - filter_graph.add_video_input_device(autosplit.settings_dict["capture_device_id"]) - width, height = filter_graph.get_input_device().get_current_format() - filter_graph.remove_filters() + resolution = get_input_device_resolution(autosplit.settings_dict["capture_device_id"]) + if resolution is not None: try: - self.capture_device.set(cv2.CAP_PROP_FRAME_WIDTH, width) - self.capture_device.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + self.capture_device.set(cv2.CAP_PROP_FRAME_WIDTH, resolution[0]) + self.capture_device.set(cv2.CAP_PROP_FRAME_HEIGHT, resolution[1]) except cv2.error: # Some cameras don't allow changing the resolution pass diff --git a/src/capture_method/XcbCaptureMethod.py b/src/capture_method/XcbCaptureMethod.py index 7f56f41f..7c957a1d 100644 --- a/src/capture_method/XcbCaptureMethod.py +++ b/src/capture_method/XcbCaptureMethod.py @@ -39,7 +39,7 @@ def get_frame(self): selection = self._autosplit_ref.settings_dict["capture_region"] x = selection["x"] + offset_x y = selection["y"] + offset_y - image = ImageGrab.grab( + image = ImageGrab.grab( # pyright: ignore[reportUnknownMemberType] # TODO: Fix upstream ( x, y, diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 168a6378..e099d444 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -1,4 +1,3 @@ -import asyncio import os import sys from collections import OrderedDict @@ -77,7 +76,7 @@ def __hash__(self): @override @staticmethod - def _generate_next_value_(name: "str | CaptureMethodEnum", *_): + def _generate_next_value_(name: str, start: int, count: int, last_values: list["str | CaptureMethodEnum"]): return name NONE = "" @@ -113,10 +112,11 @@ 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) + return super().__getitem__(key) @override def get(self, key: CaptureMethodEnum, default: object = None, /): @@ -149,7 +149,7 @@ def get(self, key: CaptureMethodEnum, default: object = None, /): CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = DesktopDuplicationCaptureMethod CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = ForceFullContentRenderingCaptureMethod elif sys.platform == "linux": - if features.check_feature(feature="xcb"): + if features.check_feature(feature="xcb"): # pyright: ignore[reportUnknownMemberType] # TODO: Fix upstream CAPTURE_METHODS[CaptureMethodEnum.XCB] = XcbCaptureMethod try: pyscreeze.screenshot() @@ -211,15 +211,22 @@ def get_input_device_resolution(index: int) -> tuple[int, int] | None: # https://github.com/Toufool/AutoSplit/issues/238 except COMError: return None - resolution = filter_graph.get_input_device().get_current_format() - filter_graph.remove_filters() + + try: + resolution = filter_graph.get_input_device().get_current_format() + # For unknown reasons, some devices can raise "ValueError: NULL pointer access". + # For instance, Oh_DeeR's AVerMedia HD Capture C985 Bus 12 + except ValueError: + return None + finally: + filter_graph.remove_filters() return resolution -async def get_all_video_capture_devices(): +def get_all_video_capture_devices(): named_video_inputs = get_input_devices() - async def get_camera_info(index: int, device_name: str): + def get_camera_info(index: int, device_name: str): backend = "" # Probing freezes some devices (like GV-USB2 and AverMedia) if already in use. See #169 # FIXME: Maybe offer the option to the user to obtain more info about their devices? @@ -246,9 +253,4 @@ async def get_camera_info(index: int, device_name: str): else None ) - return [ - camera_info - for camera_info - in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) - if camera_info is not None - ] + return list(filter(None, starmap(get_camera_info, enumerate(named_video_inputs)))) diff --git a/src/menu_bar.py b/src/menu_bar.py index c47e880d..a01fe031 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -1,4 +1,3 @@ -import asyncio import json import sys import webbrowser @@ -135,7 +134,7 @@ def __init__(self, autosplit: "AutoSplit"): self.__video_capture_devices: list[CameraInfo] = [] """ Used to temporarily store the existing cameras, - we don't want to call `get_all_video_capture_devices` agains and possibly have a different result + we don't want to call `get_all_video_capture_devices` again and possibly have a different result """ self.setupUi(self) @@ -246,7 +245,7 @@ def __fps_limit_changed(self, value: int): @fire_and_forget def __set_all_capture_devices(self): - self.__video_capture_devices = asyncio.run(get_all_video_capture_devices()) + self.__video_capture_devices = get_all_video_capture_devices() if len(self.__video_capture_devices) > 0: for i in range(self.capture_device_combobox.count()): self.capture_device_combobox.removeItem(i)