diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 737ff673..00000000 --- a/.flake8 +++ /dev/null @@ -1,37 +0,0 @@ -[flake8] -color = always -max-line-length = 120 -; Auto generated -exclude = src/gen/, typings/cv2-stubs/__init__.pyi -ignore = - ; Linebreak before binary operator - W503, - ; Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma) - E124, - ; Allow imports at the bottom of file - E402, - ; Not using typing_extensions - Y026, - ; contextlib.suppress is roughly 3x slower than try/except - SIM105, - ; False positives for attribute docstrings - CCE001, -per-file-ignores = - ; Quotes - ; Allow ... on same line as class - ; Allow ... on same line as def - ; Line too long - ; Naming conventions can't be controlled for external libraries - ; Variable names can't be controlled for external libraries - ; Argument names can't be controlled for external libraries - ; Attribute names can't be controlled for external libraries - ; False positive Class level expression with elipsis - ; Type re-exports - ; mypy 3.7 Union issue - *.pyi: Q000,E701,E704,E501,N8,A001,A002,A003,CCE002,F401,Y037 -; PyQt methods -ignore-names = closeEvent,paintEvent,keyPressEvent,mousePressEvent,mouseMoveEvent,mouseReleaseEvent -; McCabe max-complexity is also taken care of by Pylint and doesn't fail the build there -; So this is the hard limit -max-complexity = 32 -inline-quotes = double diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml index 815eb077..a7df7e76 100644 --- a/.github/workflows/lint-and-build.yml +++ b/.github/workflows/lint-and-build.yml @@ -34,21 +34,25 @@ env: GITHUB_EXCLUDE_BUILD_NUMBER: ${{ inputs.excludeBuildNumber }} jobs: - isort: + ruff: runs-on: windows-latest + strategy: + fail-fast: false + # Ruff is version and platform sensible + matrix: + python-version: ["3.9", "3.10", "3.11"] steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 - - name: Set up Python 3.11 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: ${{ matrix.python-version }} cache: "pip" cache-dependency-path: "scripts/requirements*.txt" - run: scripts/install.ps1 shell: pwsh - - name: Analysing the code with isort - run: isort src/ typings/ --check-only + - run: ruff check . add-trailing-comma: runs-on: windows-latest steps: @@ -61,19 +65,6 @@ jobs: - run: pip install add-trailing-comma - name: Analysing the code with add-trailing-comma run: add-trailing-comma $(git ls-files '**.py*') --py36-plus - Bandit: - # Bandit only matters on the version deployed. Platform checks are ignored - runs-on: windows-latest - steps: - - name: Checkout ${{ github.repository }}/${{ github.ref }} - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - run: pip install bandit - - name: Analysing the code with Bandit - run: bandit src/ -n 1 --severity-level medium --recursive Pyright: runs-on: windows-latest strategy: @@ -96,46 +87,6 @@ jobs: uses: jakebailey/pyright-action@v1 with: working-directory: src/ - Pylint: - runs-on: windows-latest - strategy: - fail-fast: false - # Pylint is version and platform sensible - matrix: - python-version: ["3.9", "3.10", "3.11"] - steps: - - name: Checkout ${{ github.repository }}/${{ github.ref }} - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: "pip" - cache-dependency-path: "scripts/requirements*.txt" - - run: scripts/install.ps1 - shell: pwsh - - name: Analysing the code with Pylint - run: pylint src/ --reports=y --output-format=colorized - Flake8: - runs-on: windows-latest - strategy: - fail-fast: false - # Flake8 is tied to the version of Python on which it runs. Platform checks are ignored - matrix: - python-version: ["3.9", "3.10", "3.11"] - steps: - - name: Checkout ${{ github.repository }}/${{ github.ref }} - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: "pip" - cache-dependency-path: "scripts/requirements*.txt" - - run: scripts/install.ps1 - shell: pwsh - - name: Analysing the code with Flake8 - run: flake8 src/ typings/ Build: runs-on: windows-latest strategy: diff --git a/.gitignore b/.gitignore index 84c492a0..40e3ff25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Caches -.cache/ +.*cache/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a5cf6e6f..6e71f39c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,16 +6,12 @@ "eamodio.gitlens", "emeraldwalk.runonsave", "ms-python.autopep8", - "ms-python.flake8", - "ms-python.isort", - "ms-python.pylint", "ms-python.python", "ms-python.vscode-pylance", "ms-vscode.powershell", "pkief.material-icon-theme", "redhat.vscode-xml", "redhat.vscode-yaml", - "shardulm94.trailing-spaces", ], "unwantedRecommendations": [ // Must disable in this workspace // @@ -24,6 +20,7 @@ // VSCode has implemented an optimized version "coenraads.bracket-pair-colorizer", "coenraads.bracket-pair-colorizer-2", + "shardulm94.trailing-spaces", // Obsoleted by Pylance "ms-pyright.pyright", // Not configurable per workspace, tends to conflict with other linters @@ -31,6 +28,10 @@ // // Don't recommend to autoinstall // // + // Use Ruff instead + "ms-python.flake8", + "ms-python.isort", + "ms-python.pylint", // We use autopep8 "ms-python.black-formatter", // This is a Git project diff --git a/.vscode/settings.json b/.vscode/settings.json index b77a08d2..8405b12c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,11 +8,6 @@ 72 ] }, - "trailing-spaces.includeEmptyLines": true, - "trailing-spaces.trimOnSave": true, - "trailing-spaces.syntaxIgnore": [ - "markdown" - ], "[markdown]": { "files.trimTrailingWhitespace": false, }, @@ -28,14 +23,10 @@ "source.fixAll": true, "source.fixAll.unusedImports": false, "source.fixAll.convertImportFormat": true, - "source.organizeImports": true, + "source.organizeImports": false, }, "emeraldwalk.runonsave": { "commands": [ - { - "match": "\\.pyi?", - "cmd": "unify ${file} --in-place --quote=\"\\\"\"" - }, { "match": "\\.pyi?", "cmd": "add-trailing-comma ${file} --py36-plus" @@ -62,9 +53,13 @@ "**/*.code-search": true, "*.lock": true, }, + // Set the default formatter to help avoid Prettier + "[json][jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features", + }, "[python]": { - // Cannot use autotpep8 until https://github.com/microsoft/vscode-autopep8/issues/32 is fixed - "editor.defaultFormatter": "ms-python.python", + // Ruff is not yet a formatter: https://github.com/charliermarsh/ruff/issues/1904 + "editor.defaultFormatter": "ms-python.autopep8", "editor.tabSize": 4, "editor.rulers": [ 72, // PEP8-17 docstrings @@ -77,60 +72,20 @@ // Important to follow the config in pyrightconfig.json "python.analysis.useLibraryCodeForTypes": false, "python.analysis.diagnosticMode": "workspace", - "python.formatting.provider": "autopep8", - "isort.check": true, - "isort.importStrategy": "fromEnvironment", "python.linting.enabled": true, - // Use the new Pylint extension instead - "python.linting.pylintEnabled": false, - "pylint.severity": { - "convention": "Warning", - "error": "Error", - "fatal": "Error", - "refactor": "Warning", - "warning": "Warning", - "info": "Information" - }, - // Use the new Flake8 extension instead + "ruff.importStrategy": "fromEnvironment", + // Use the Ruff extension instead + "isort.check": false, + "python.linting.banditEnabled": false, "python.linting.flake8Enabled": false, - // Partial codes don't work yet: https://github.com/microsoft/vscode-flake8/issues/7 - "flake8.severity": { - "convention": "Warning", - "error": "Error", - "fatal": "Error", - "refactor": "Warning", - "warning": "Warning", - "info": "Warning", - // builtins - "A": "Warning", - // mccabe - "C": "Warning", - // class attributes order - "CCE": "Warning", - // pycodestyles - "E": "Warning", - "E9": "Error", // Runtime - "W": "Warning", - "W6": "Error", // Deprecation warning - // Pyflakes - "F": "Warning", - // PEP8 Naming convention - "N": "Warning", - // Simplify - "SIM": "Warning", - "SIM9": "Information", - // PYI - "Y": "Warning", - }, - // PyRight obsoletes mypy - "python.linting.mypyEnabled": false, - // Is already wrapped by Flake8, prospector and pylama - "python.linting.pycodestyleEnabled": false, - // Just another wrapper, use Flake8 OR this "python.linting.prospectorEnabled": false, - // Just another wrapper, use Flake8 OR this + "python.linting.pycodestyleEnabled": false, "python.linting.pylamaEnabled": false, - "python.linting.banditEnabled": true, + "python.linting.pylintEnabled": false, + // Use the autopep8 extension instead + "python.formatting.provider": "none", + // Use Pyright/Pylance instead + "python.linting.mypyEnabled": false, "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", "powershell.codeFormatting.autoCorrectAliases": true, "powershell.codeFormatting.trimWhitespaceAroundPipe": true, diff --git a/PyInstaller/hooks/hook-requests.py b/PyInstaller/hooks/hook-requests.py index 13de4b6b..e1a554d0 100644 --- a/PyInstaller/hooks/hook-requests.py +++ b/PyInstaller/hooks/hook-requests.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from PyInstaller.utils.hooks import collect_data_files # Get the cacert.pem diff --git a/pyproject.toml b/pyproject.toml index 2350488c..b84e0c21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,105 @@ +# https://beta.ruff.rs/docs/configuration +[tool.ruff] +target-version = "py39" +line-length = 120 +select = ["ALL"] +# https://beta.ruff.rs/docs/rules +ignore = [ + ### + # Not needed or wanted + ### + "D1", # pydocstyle Missing doctring + "D401", # pydocstyle: non-imperative-mood + "EM", # flake8-errmsg + "FBT", # flake8-boolean-trap + "INP", # flake8-no-pep420 + "ISC003", # flake8-implicit-str-concat: explicit-string-concatenation + # Short messages are still considered "long" messages + "TRY003", # tryceratops : raise-vanilla-args + # Don't remove commented code, also too inconsistant + "ERA001", # eradicate: commented-out-code + # contextlib.suppress is roughly 3x slower than try/except + "SIM105", # flake8-simplify: use-contextlib-suppress + # Checked by type-checker (pyright) + "ANN", # flake-annotations + "TCH", # flake8-type-checking + # Already shown by Pylance, checked by pyright, and can be caused by overloads. + "ARG002", # Unused method argument + # We want D213: multi-line-summary-second-line and D211: no-blank-line-before-class + "D203", # pydocstyle: one-blank-line-before-class + "D212", # pydocstyle: multi-line-summary-first-line + # Allow differentiating between broken (FIXME) and to be done/added/completed (TODO) + "TD001", # flake8-todos: invalid-todo-tag + # Not all TODOs are worth an issue, this would be better as a warning + "TD003", # flake8-todos: missing-todo-link + + ### + # Specific to this project + ### + # We have some Pascal case module names + "N999", # pep8-naming: Invalid module name + # Print are used as debug logs + "T20", # flake8-print + "D205", # Not all docstrings have a short description + desrciption + # This is a relatively small, low contributors project. Git blame suffice. + "TD002", + + ### FIXME (no warnings in Ruff yet: https://github.com/charliermarsh/ruff/issues/1256): + "PTH", +] + +[tool.ruff.per-file-ignores] +"typings/**/*.pyi" = [ + "F821", # https://github.com/charliermarsh/ruff/issues/3011 + # The following can't be controlled for external libraries: + "N8", # Naming conventions + "A", # Shadowing builtin names + "PLR0913", # Argument count + "PYI042", # CamelCase TypeAlias +] + +# https://beta.ruff.rs/docs/settings/#flake8-implicit-str-concat +[tool.ruff.flake8-implicit-str-concat] +allow-multiline = false + +# https://beta.ruff.rs/docs/settings/#isort +[tool.ruff.isort] +combine-as-imports = true +split-on-trailing-comma = false +required-imports = ["from __future__ import annotations"] +# Unlike isort, Ruff only counts relative imports as local-folder by default for know. +# https://github.com/charliermarsh/ruff/issues/2419 +# https://github.com/charliermarsh/ruff/issues/3115 +known-local-folder = [ + "capture_method", + "gen", + "AutoControlledWorker", + "AutoSplit", + "AutoSplitImage", + "compare", + "error_messages", + "error_messages", + "hotkeys", + "menu_bar", + "region_selection", + "split_parser", + "user_profile", + "utils", +] + +# https://beta.ruff.rs/docs/settings/#mccabe +[tool.ruff.mccabe] +# Hard limit, arbitrary to 4 bytes +max-complexity = 31 +# Arbitrary to 2 bytes, same as SonarLint +# max-complexity = 15 + +[tool.ruff.pylint] +# Arbitrary to 1 byte, same as SonarLint +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] @@ -6,7 +108,7 @@ recursive = true aggressive = 3 ignore = [ "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma) - "E70" # Allow ... on same line as def + "E70", # Allow ... on same line as def ] # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file @@ -17,7 +119,7 @@ enableTypeIgnoreComments = false # Extra strict reportImplicitStringConcatenation = "error" reportCallInDefaultInitializer = "error" -reportMissingSuperCall = "none" # False positives on base classes +reportMissingSuperCall = "none" # False positives on base classes reportPropertyTypeMismatch = "error" reportUninitializedInstanceVariable = "error" reportUnnecessaryTypeIgnoreComment = "error" @@ -36,106 +138,11 @@ reportUnusedCallResult = "none" reportMissingTypeStubs = "warning" # False positives with TYPE_CHECKING reportImportCycles = "information" -# False positives with PyQt .connect. pylint(no-member) works instead +# False positives with PyQt .connect reportFunctionMemberAccess = "none" # Extra runtime safety reportUnnecessaryComparison = "warning" -# Flake8 does a better job +# Using Flake8/Ruff instead reportUnusedImport = "none" # numpy has way too many complex types that triggers this reportUnknownMemberType = "none" - -# https://github.com/PyCQA/pylint/blob/main/examples/pylintrc -# https://pylint.pycqa.org/en/latest/technical_reference/features.html -[tool.pylint.REPORTS] -# Just like default but any error, warning or convention will make drop to 9 or less. refactor are worth more -evaluation = "10.0 - error - warning - convention - ((10 * refactor) / statement) * 10" -[tool.pylint.MASTER] -fail-under = 9.0 -# https://pydocbrowser.github.io/pylint/latest/pylint.extensions.html -# https://pylint.pycqa.org/en/latest/technical_reference/extensions.html -load-plugins = [ - "pylint.extensions.bad_builtin", - "pylint.extensions.check_elif", - "pylint.extensions.comparison_placement", - "pylint.extensions.confusing_elif", - "pylint.extensions.consider_ternary_expression", - "pylint.extensions.empty_comment", - "pylint.extensions.emptystring", - "pylint.extensions.for_any_all", - "pylint.extensions.eq_without_hash", - "pylint.extensions.mccabe", - "pylint.extensions.overlapping_exceptions", - "pylint.extensions.private_import", - # "pylint.extensions.redefined_loop_name", # 2.16 - "pylint.extensions.redefined_variable_type", - "pylint.extensions.set_membership", - "pylint.extensions.typing", - # TODO: Maybe later - # "pylint.extensions.docparams", - # Not wanted/needed - # "pylint.extensions.broad_try_clause", - # "pylint.extensions.code_style", - # "pylint.extensions.comparetozero", - # "pylint.extensions.docstyle", - # "pylint.extensions.no_self_use", - # "pylint.extensions.while_used", -] -ignore-paths = [ - # Auto generated - "^src/gen/.*$", - # We expect stub files to be incomplete or contain useless statements - "^.*.pyi$", -] -extension-pkg-allow-list = ["PyQt6", "PySide6", "win32ui", "win32.win32gui"] - -[tool.pylint.FORMAT] -max-line-length = 120 - -[tool.pylint.DESIGN] -# Same as SonarLint -max-args = 7 -# Arbitrary to 2 bytes -max-attributes = 15 -max-locals = 15 - -[tool.pylint.'MESSAGES CONTROL'] -# Same as SonarLint -max-complexity = 15 -# At least same as max-complexity -max-branches = 15 -# https://github.com/PyCQA/pep8-naming/issues/164 -# OR doesn't fit CaptureMethodMeta -valid-classmethod-first-arg = "self" -disable = [ - # No need to mention the fixmes - "fixme", - "missing-docstring", - # Already taken care of by isort - "ungrouped-imports", - "unused-import", - "wrong-import-order", - "wrong-import-position", - # Already taken care of by Flake8-naming, which does a better job - "invalid-name", - # Already taken care of and grayed out. Also conflicts with Pylance reportIncompatibleMethodOverride - "unused-argument", - # Only reports a single instance. Pyright does a better job anyway - "cyclic-import", - # Similar lines in 2 files, doesn't really work - "R0801", -] - -[tool.pylint.TYPECHECK] -generated-members = [ - # https://github.com/PyCQA/pylint/issues/4987 - "cv2", - # https://github.com/mhammond/pywin32/issues/1913 - "pywintypes.error", -] - -[tool.isort] -line_length = 120 -combine_as_imports = true -include_trailing_comma = true -multi_line_output = 5 diff --git a/scripts/install.ps1 b/scripts/install.ps1 index a7ed1534..cac7c468 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -10,10 +10,6 @@ $dev = If ($Env:GITHUB_JOB -eq 'Build') { '' } Else { '-dev' } # Ensures installation tools are up to date. This also aliases pip to pip3 on MacOS. python3 -m pip install wheel pip setuptools --upgrade pip install -r "$PSScriptRoot/requirements$dev.txt" --upgrade -# Don't install pyright on CI. We use an action -if (-not $Env:CI -and (Get-Command 'npm' -ErrorAction SilentlyContinue)) { - npm i --global pyright@latest -} # Don't compile resources on the Build CI job as it'll do so in build script If ($dev) { diff --git a/scripts/lint.ps1 b/scripts/lint.ps1 index b45edcf1..b59e502e 100644 --- a/scripts/lint.ps1 +++ b/scripts/lint.ps1 @@ -2,50 +2,29 @@ $originalDirectory = $pwd Set-Location "$PSScriptRoot/.." $exitCodes = 0 -Write-Host "`nRunning autofixes..." -isort src/ typings/ +Write-Host "`nRunning formatting..." autopep8 $(git ls-files '**.py*') --in-place -unify src/ --recursive --in-place --quote='"' add-trailing-comma $(git ls-files '**.py*') --py36-plus -Write-Host "`nRunning Pyright..." -$Env:PYRIGHT_PYTHON_FORCE_VERSION = 'latest' -pyright src/ -$exitCodes += $LastExitCode -if ($LastExitCode -gt 0) { - Write-Host "`Pyright failed ($LastExitCode)" -ForegroundColor Red -} -else { - Write-Host "`Pyright passed" -ForegroundColor Green -} - -Write-Host "`nRunning Pylint..." -pylint src/ --output-format=colorized +Write-Host "`nRunning Ruff..." +ruff check . --fix $exitCodes += $LastExitCode if ($LastExitCode -gt 0) { - Write-Host "`Pylint failed ($LastExitCode)" -ForegroundColor Red + Write-Host "`Ruff failed ($LastExitCode)" -ForegroundColor Red } else { - Write-Host "`Pylint passed" -ForegroundColor Green + Write-Host "`Ruff passed" -ForegroundColor Green } -Write-Host "`nRunning Flake8..." -flake8 src/ typings/ +Write-Host "`nRunning Pyright..." +$Env:PYRIGHT_PYTHON_FORCE_VERSION = 'latest' +npx pyright@latest src/ $exitCodes += $LastExitCode if ($LastExitCode -gt 0) { - Write-Host "`Flake8 failed ($LastExitCode)" -ForegroundColor Red -} -else { - Write-Host "`Flake8 passed" -ForegroundColor Green -} - -Write-Host "`nRunning Bandit..." -bandit src/ -f custom --silent --recursive -if ($LastExitCode -gt 0) { - Write-Host "`Bandit warning ($LastExitCode)" -ForegroundColor Yellow + Write-Host "`Pyright failed ($LastExitCode)" -ForegroundColor Red } else { - Write-Host "`Bandit passed" -ForegroundColor Green + Write-Host "`Pyright passed" -ForegroundColor Green } if ($exitCodes -gt 0) { diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index 34be909c..aeb7a1e6 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -6,24 +6,10 @@ # Dependencies -r requirements.txt # -# Linters -bandit -flake8>=6 # Validates configuration -flake8-builtins -flake8-bugbear -flake8-class-attributes-order -flake8-comprehensions>=3.8 # flake8 v5 support -flake8-datetimez -flake8-noqa>=1.3.0 # flake8 v6 support -flake8-pyi>=22.11.0 # flake8 v6 support -flake8-simplify -pep8-naming -pylint>=2.14,<3.0.0 # New checks # 2.16 and 3.0 still in pre-release -# Formatters +# Linters & Formatters add-trailing-comma>=2.3.0 # Added support for with statement autopep8>=2.0.0 # New checks -isort -unify +ruff>=0.0.269 # New TODO and PYI violations # # Run `./scripts/designer.ps1` to quickly open the bundled PyQt Designer. # Can also be downloaded externally as a non-python package @@ -38,4 +24,3 @@ types-pyinstaller types-pywin32 types-requests types-toml -typing-extensions diff --git a/scripts/requirements.txt b/scripts/requirements.txt index c666507d..8f841ebf 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -15,6 +15,7 @@ PyAutoGUI PyQt6>=6.4.2 # Python 3.11 support requests<=2.28.1 # 2.28.2 has issues with PyInstaller https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/534 toml +typing-extensions>=4.4.0 # @override decorator support # # Build and compile resources pyinstaller>=5.5 # Python 3.11 support diff --git a/src/AutoSplit.py b/src/AutoSplit.py index b15c3f2f..bfa1d949 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# -*- coding: utf-8 -*- from __future__ import annotations import ctypes @@ -16,6 +15,7 @@ from PyQt6 import QtCore, QtGui from PyQt6.QtTest import QTest from PyQt6.QtWidgets import QApplication, QFileDialog, QLabel, QMainWindow, QMessageBox, QWidget +from typing_extensions import override import error_messages import user_profile @@ -25,14 +25,26 @@ from gen import about, design, settings, update_checker from hotkeys import HOTKEYS, after_setting_hotkey, send_command from menu_bar import ( - about_qt, about_qt_for_python, check_for_updates, get_default_settings_from_ui, open_about, open_settings, - open_update_checker, view_help, + about_qt, + about_qt_for_python, + check_for_updates, + get_default_settings_from_ui, + open_about, + open_settings, + open_update_checker, + view_help, ) from region_selection import align_region, select_region, select_window, validate_before_parsing from split_parser import BELOW_FLAG, DUMMY_FLAG, PAUSE_FLAG, parse_and_validate_images from user_profile import DEFAULT_PROFILE from utils import ( - AUTOSPLIT_VERSION, FIRST_WIN_11_BUILD, FROZEN, WINDOWS_BUILD_NUMBER, auto_split_directory, decimal, is_valid_image, + AUTOSPLIT_VERSION, + FIRST_WIN_11_BUILD, + FROZEN, + WINDOWS_BUILD_NUMBER, + auto_split_directory, + decimal, + is_valid_image, open_file, ) @@ -44,7 +56,7 @@ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) -class AutoSplit(QMainWindow, design.Ui_MainWindow): # pylint: disable=too-many-instance-attributes +class AutoSplit(QMainWindow, design.Ui_MainWindow): # Parse command line args is_auto_controlled = "--auto-controlled" in sys.argv @@ -100,11 +112,11 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow): # pylint: disable=too-many- split_image: AutoSplitImage | None = None update_auto_control: QtCore.QThread | None = None - def __init__(self, parent: QWidget | None = None): # pylint: disable=too-many-statements + def __init__(self, parent: QWidget | None = None): # noqa: PLR0915 super().__init__(parent) # Setup global error handling - self.show_error_signal.connect(lambda errorMessageBox: errorMessageBox()) + self.show_error_signal.connect(lambda error_message_box: error_message_box()) sys.excepthook = error_messages.make_excepthook(self) self.setupUi(self) @@ -215,7 +227,7 @@ def __init__(self, parent: QWidget | None = None): # pylint: disable=too-many-s self.show() try: - import pyi_splash # pyright: ignore[reportMissingModuleSource] # pylint: disable=import-outside-toplevel + import pyi_splash # pyright: ignore[reportMissingModuleSource] pyi_splash.close() except ModuleNotFoundError: pass @@ -247,11 +259,8 @@ def __browse(self): def __update_live_image_details(self, capture: cv2.Mat | None, called_from_timer: bool = False): # HACK: Since this is also called in __get_capture_for_comparison, # we don't need to update anything if the app is running - if called_from_timer: - if self.is_running or self.start_image: - return - else: - capture, _ = self.capture_method.get_frame(self) + if called_from_timer and not (self.is_running or self.start_image): + capture, _ = self.capture_method.get_frame(self) # Update title from target window or Capture Device name capture_region_window_label = self.settings_dict["capture_device_name"] \ @@ -267,9 +276,7 @@ def __update_live_image_details(self, capture: cv2.Mat | None, called_from_timer set_preview_image(self.live_image, capture, False) def __load_start_image(self, started_by_button: bool = False, wait_for_delay: bool = True): - """ - Not thread safe (if triggered by LiveSplit for example). Use `load_start_image_signal.emit` instead. - """ + """Not thread safe (if triggered by LiveSplit for example). Use `load_start_image_signal.emit` instead.""" self.timer_start_image.stop() self.current_image_file_label.setText("-") self.start_image_status_value_label.setText("not found") @@ -341,8 +348,10 @@ def __start_image_function(self): if below_flag and not self.split_below_threshold and similarity_diff >= 0: self.split_below_threshold = True return - if (below_flag and self.split_below_threshold and similarity_diff < 0 and is_valid_image(capture)) \ - or (not below_flag and similarity_diff >= 0): # pylint: disable=too-many-boolean-expressions + if ( + (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 @@ -441,9 +450,7 @@ def __is_current_split_out_of_range(self): 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 - """ + """"Undo Split" and "Prev. Img." buttons connect to here.""" # Can't undo until timer is started # or Undoing past the first image if not self.is_running \ @@ -465,9 +472,7 @@ def undo_split(self, navigate_image_only: bool = False): send_command(self, "undo") def skip_split(self, navigate_image_only: bool = False): - """ - "Skip Split" and "Next Img." buttons connect to here - """ + """"Skip Split" and "Next Img." buttons connect to here.""" # Can't skip or split until timer is started # or Splitting/skipping when there are no images left if not self.is_running \ @@ -489,11 +494,11 @@ def skip_split(self, navigate_image_only: bool = False): send_command(self, "skip") def pause(self): - # TODO add what to do when you hit pause hotkey, if this even needs to be done + # TODO: add what to do when you hit pause hotkey, if this even needs to be done pass def reset(self): - """" + """ When the reset button or hotkey is pressed, it will set `is_running` to False, which will trigger in the __auto_splitter function, if running, to abort and change GUI. """ @@ -507,21 +512,19 @@ def start_auto_splitter(self): return start_label: str = self.start_image_status_value_label.text() - if start_label.endswith("ready") or start_label.endswith("paused"): + if start_label.endswith(("ready", "paused")): self.start_image_status_value_label.setText("not ready") self.start_auto_splitter_signal.emit() def __check_for_reset_state_update_ui(self): - """ - Check if AutoSplit is started, if not either restart (loop splits) or update the GUI - """ + """Check if AutoSplit is started, if not either restart (loop splits) or update the GUI.""" if not self.is_running: self.gui_changes_on_reset(True) return True return False - def __auto_splitter(self): + def __auto_splitter(self): # noqa: PLR0912,PLR0915 if not self.settings_dict["split_hotkey"] and not self.is_auto_controlled: self.gui_changes_on_reset(True) error_messages.split_hotkey() @@ -695,9 +698,7 @@ def __similarity_threshold_loop(self, number_of_split_images: int, dummy_splits_ QTest.qWait(wait_delta_ms) continue - elif ( # pylint: disable=confusing-consecutive-elif - below_flag and self.split_below_threshold and is_valid_image(capture) - ): + elif below_flag and self.split_below_threshold and is_valid_image(capture): self.split_below_threshold = False break @@ -794,9 +795,7 @@ def gui_changes_on_reset(self, safe_to_reload_start_image: bool = False): self.load_start_image_signal[bool, bool].emit(False, False) def __get_capture_for_comparison(self): - """ - Grab capture region and resize for comparison - """ + """Grab capture region and resize for comparison.""" capture, is_old_image = self.capture_method.get_frame(self) # This most likely means we lost capture @@ -815,9 +814,7 @@ def __get_capture_for_comparison(self): return capture, is_old_image def __reset_if_should(self, capture: cv2.Mat | None): - """ - Checks if we should reset, resets if it's the case, and returns the result - """ + """Checks if we should reset, resets if it's the case, and returns the result.""" if self.reset_image: if self.settings_dict["enable_auto_reset"]: similarity = self.reset_image.compare_with_capture(self, capture) @@ -877,10 +874,9 @@ def __update_split_image(self, specific_image: AutoSplitImage | None = None): loop_tuple = self.split_images_and_loop_number[self.split_image_number] self.image_loop_value_label.setText(f"{loop_tuple[1]}/{loop_tuple[0].loops}") + @override def closeEvent(self, a0: QtGui.QCloseEvent | None = None): - """ - Exit safely when closing the window - """ + """Exit safely when closing the window.""" def exit_program() -> NoReturn: if self.update_auto_control: @@ -986,7 +982,7 @@ def main(): timer.start(500) exit_code = app.exec() - except Exception as exception: # pylint: disable=broad-except # We really want to catch everything here + except Exception as exception: # noqa: BLE001 # We really want to catch everything here error_messages.handle_top_level_exceptions(exception) # Catch Keyboard Interrupts for a clean close diff --git a/src/AutoSplitImage.py b/src/AutoSplitImage.py index 7adc0b24..fd2ae8d9 100644 --- a/src/AutoSplitImage.py +++ b/src/AutoSplitImage.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from enum import Enum +from enum import IntEnum from math import sqrt from typing import TYPE_CHECKING @@ -9,8 +9,8 @@ import numpy as np import error_messages -from compare import check_if_image_has_transparency, compare_histograms, compare_l2_norm, compare_phash -from utils import MAXBYTE, is_valid_image +from compare import COMPARE_METHODS_BY_INDEX, check_if_image_has_transparency +from utils import MAXBYTE, RGB_CHANNEL_COUNT, ColorChannel, ImageShape, is_valid_image if TYPE_CHECKING: from AutoSplit import AutoSplit @@ -26,7 +26,7 @@ RESET_KEYWORD = "reset" -class ImageType(Enum): +class ImageType(IntEnum): SPLIT = 0 RESET = 1 START = 2 @@ -49,36 +49,28 @@ class AutoSplitImage(): __similarity_threshold: float | None = None def get_delay_time(self, default: AutoSplit | int): - """ - Get image's delay time or fallback to the default value from spinbox - """ + """Get image's delay time or fallback to the default value from spinbox.""" default_value = default \ if isinstance(default, int) \ else default.settings_dict["default_delay_time"] return default_value if self.__delay_time is None else self.__delay_time def __get_comparison_method(self, default: AutoSplit | int): - """ - Get image's comparison or fallback to the default value from combobox - """ + """Get image's comparison or fallback to the default value from combobox.""" default_value = default \ if isinstance(default, int) \ else default.settings_dict["default_comparison_method"] return default_value if self.__comparison_method is None else self.__comparison_method def get_pause_time(self, default: AutoSplit | float): - """ - Get image's pause time or fallback to the default value from spinbox - """ + """Get image's pause time or fallback to the default value from spinbox.""" default_value = default \ if isinstance(default, float) \ else default.settings_dict["default_pause_time"] return default_value if self.__pause_time is None else self.__pause_time def get_similarity_threshold(self, default: AutoSplit | float): - """ - Get image's similarity threshold or fallback to the default value from spinbox - """ + """Get image's similarity threshold or fallback to the default value from spinbox.""" default_value = default \ if isinstance(default, float) \ else default.settings_dict["default_similarity_threshold"] @@ -116,7 +108,7 @@ def __read_image_bytes(self, path: str): # the number of nonzero elements in the alpha channel of the split image. # This may result in images bigger than COMPARISON_RESIZE if there's plenty of transparency. # Which wouldn't incur any performance loss in methods where masked regions are ignored. - scale = min(1, sqrt(COMPARISON_RESIZE_AREA / cv2.countNonZero(image[:, :, 3]))) + scale = min(1, sqrt(COMPARISON_RESIZE_AREA / cv2.countNonZero(image[:, :, ColorChannel.Alpha]))) image = cv2.resize( image, @@ -131,7 +123,7 @@ def __read_image_bytes(self, path: str): else: image = cv2.resize(image, COMPARISON_RESIZE, interpolation=cv2.INTER_NEAREST) # Add Alpha channel if missing - if image.shape[2] == 3: + if image.shape[ImageShape.Channels] == RGB_CHANNEL_COUNT: image = cv2.cvtColor(image, cv2.COLOR_BGR2BGRA) self.byte_array = image @@ -144,25 +136,24 @@ def compare_with_capture( default: AutoSplit | int, capture: cv2.Mat | None, ): - """ - Compare image with capture using image's comparison method. Falls back to combobox - """ - + """Compare image with capture using image's comparison method. Falls back to combobox.""" if not is_valid_image(self.byte_array) or not is_valid_image(capture): return 0.0 capture = cv2.resize(capture, self.byte_array.shape[1::-1]) comparison_method = self.__get_comparison_method(default) - if comparison_method == 0: - return compare_l2_norm(self.byte_array, capture, self.mask) - if comparison_method == 1: - return compare_histograms(self.byte_array, capture, self.mask) - if comparison_method == 2: - return compare_phash(self.byte_array, capture, self.mask) - return 0.0 + + return COMPARE_METHODS_BY_INDEX.get(comparison_method, compare_dummy)(self.byte_array, capture, self.mask) + + +def compare_dummy(*_: object): return 0.0 -if True: # pylint: disable=using-constant-test +if True: from split_parser import ( - comparison_method_from_filename, delay_time_from_filename, flags_from_filename, loop_from_filename, - pause_from_filename, threshold_from_filename, + comparison_method_from_filename, + delay_time_from_filename, + flags_from_filename, + loop_from_filename, + pause_from_filename, + threshold_from_filename, ) diff --git a/src/capture_method/BitBltCaptureMethod.py b/src/capture_method/BitBltCaptureMethod.py index 385bc7f1..ed7b4e7f 100644 --- a/src/capture_method/BitBltCaptureMethod.py +++ b/src/capture_method/BitBltCaptureMethod.py @@ -12,7 +12,7 @@ from win32 import win32gui from capture_method.CaptureMethodBase import CaptureMethodBase -from utils import get_window_bounds, is_valid_hwnd, try_delete_dc +from utils import RGBA_CHANNEL_COUNT, get_window_bounds, is_valid_hwnd, try_delete_dc if TYPE_CHECKING: from AutoSplit import AutoSplit @@ -56,7 +56,7 @@ def get_frame(self, autosplit: AutoSplit) -> tuple[cv2.Mat | None, bool]: win32con.SRCCOPY, ) image = np.frombuffer(cast(bytes, bitmap.GetBitmapBits(True)), dtype=np.uint8) - image.shape = (selection["height"], selection["width"], 4) + image.shape = (selection["height"], selection["width"], RGBA_CHANNEL_COUNT) except (win32ui.error, pywintypes.error): # Invalid handle or the window was closed while it was being manipulated return None, False diff --git a/src/capture_method/CaptureMethodBase.py b/src/capture_method/CaptureMethodBase.py index 41e6d935..b391fe45 100644 --- a/src/capture_method/CaptureMethodBase.py +++ b/src/capture_method/CaptureMethodBase.py @@ -17,7 +17,7 @@ def __init__(self, autosplit: AutoSplit | None): def reinitialize(self, autosplit: AutoSplit): self.close(autosplit) - self.__init__(autosplit) # pylint: disable=unnecessary-dunder-call + self.__init__(autosplit) def close(self, autosplit: AutoSplit): # Some capture methods don't need an initialization process @@ -26,7 +26,7 @@ def close(self, autosplit: AutoSplit): def get_frame(self, autosplit: AutoSplit) -> tuple[cv2.Mat | None, bool]: """ Captures an image of the region for a window matching the given - parameters of the bounding box + parameters of the bounding box. @return: The image of the region in the window in BGRA format """ diff --git a/src/capture_method/DesktopDuplicationCaptureMethod.py b/src/capture_method/DesktopDuplicationCaptureMethod.py index 6c322a49..c7742cff 100644 --- a/src/capture_method/DesktopDuplicationCaptureMethod.py +++ b/src/capture_method/DesktopDuplicationCaptureMethod.py @@ -15,7 +15,7 @@ from AutoSplit import AutoSplit -class DesktopDuplicationCaptureMethod(BitBltCaptureMethod): # pylint: disable=too-few-public-methods +class DesktopDuplicationCaptureMethod(BitBltCaptureMethod): def __init__(self, autosplit: AutoSplit | None): super().__init__(autosplit) # Must not set statically as some laptops will throw an error diff --git a/src/capture_method/ForceFullContentRenderingCaptureMethod.py b/src/capture_method/ForceFullContentRenderingCaptureMethod.py index ea2acd76..384ef027 100644 --- a/src/capture_method/ForceFullContentRenderingCaptureMethod.py +++ b/src/capture_method/ForceFullContentRenderingCaptureMethod.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from capture_method.BitBltCaptureMethod import BitBltCaptureMethod -class ForceFullContentRenderingCaptureMethod(BitBltCaptureMethod): # pylint: disable=too-few-public-methods +class ForceFullContentRenderingCaptureMethod(BitBltCaptureMethod): _render_full_content = True diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py index d53c8ded..defd2727 100644 --- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py +++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py @@ -59,7 +59,7 @@ def __read_loop(self, autosplit: AutoSplit): self.last_captured_frame = image self.is_old_image = False - except Exception as exception: # pylint: disable=broad-except # We really want to catch everything here + except Exception as exception: # noqa: BLE001 # We really want to catch everything here error = exception self.capture_device.release() autosplit.show_error_signal.emit( @@ -119,7 +119,7 @@ def get_frame(self, autosplit: AutoSplit): return cv2.cvtColor(image, cv2.COLOR_BGR2BGRA), is_old_image def recover_window(self, captured_window_title: str, autosplit: AutoSplit) -> bool: - raise NotImplementedError() + raise NotImplementedError def check_selected_region_exists(self, autosplit: AutoSplit): return bool(self.capture_device.isOpened()) diff --git a/src/capture_method/WindowsGraphicsCaptureMethod.py b/src/capture_method/WindowsGraphicsCaptureMethod.py index 59fdd2e7..68315f8e 100644 --- a/src/capture_method/WindowsGraphicsCaptureMethod.py +++ b/src/capture_method/WindowsGraphicsCaptureMethod.py @@ -13,7 +13,7 @@ from winsdk.windows.graphics.imaging import BitmapBufferAccessMode, SoftwareBitmap from capture_method.CaptureMethodBase import CaptureMethodBase -from utils import WINDOWS_BUILD_NUMBER, get_direct3d_device, is_valid_hwnd +from utils import RGBA_CHANNEL_COUNT, WINDOWS_BUILD_NUMBER, get_direct3d_device, is_valid_hwnd if TYPE_CHECKING: from AutoSplit import AutoSplit @@ -64,8 +64,7 @@ def close(self, autosplit: AutoSplit): except OSError: # OSError: The application called an interface that was marshalled for a different thread # This still seems to close the session and prevent the following hard crash in LiveSplit - # pylint: disable=line-too-long - # "AutoSplit.exe " # noqa: E501 + # "AutoSplit.exe " # noqa: E501 pass self.session = None @@ -107,7 +106,7 @@ async def coroutine(): raise ValueError("Unable to obtain the BitmapBuffer from SoftwareBitmap.") reference = bitmap_buffer.create_reference() image = np.frombuffer(cast(bytes, reference), dtype=np.uint8) - image.shape = (self.size.height, self.size.width, 4) + image.shape = (self.size.height, self.size.width, RGBA_CHANNEL_COUNT) image = image[ selection["y"]:selection["y"] + selection["height"], selection["x"]:selection["x"] + selection["width"], @@ -122,7 +121,7 @@ def recover_window(self, captured_window_title: str, autosplit: AutoSplit): autosplit.hwnd = hwnd self.close(autosplit) try: - self.__init__(autosplit) # pylint: disable=unnecessary-dunder-call + self.__init__(autosplit) # Unrecordable hwnd found as the game is crashing except OSError as exception: if str(exception).endswith("The parameter is incorrect"): diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index c8fe942b..cd2c2553 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -6,7 +6,7 @@ from enum import Enum, EnumMeta, unique from typing import TYPE_CHECKING, TypedDict, cast -from _ctypes import COMError # pylint: disable=C2701 +from _ctypes import COMError from pygrabber.dshow_graph import FilterGraph from capture_method.BitBltCaptureMethod import BitBltCaptureMethod @@ -45,7 +45,7 @@ class CaptureMethodMeta(EnumMeta): # Allow checking if simple string is enum def __contains__(self, other: str): try: - self(other) # pylint: disable=no-value-for-parameter + self(other) except ValueError: return False return True @@ -76,9 +76,7 @@ def __hash__(self): class CaptureMethodDict(OrderedDict[CaptureMethodEnum, CaptureMethodInfo]): def get_index(self, capture_method: str | CaptureMethodEnum): - """ - Returns 0 if the capture_method is invalid or unsupported - """ + """Returns 0 if the capture_method is invalid or unsupported.""" try: return list(self.keys()).index(cast(CaptureMethodEnum, capture_method)) except ValueError: @@ -96,8 +94,8 @@ def get_method_by_index(self, index: int): return first(self) return list(self.keys())[index] - if TYPE_CHECKING: # noqa: CCE002 - __getitem__ = None # pyright: ignore[reportGeneralTypeIssues] # Disallow unsafe get + if TYPE_CHECKING: + __getitem__ = None # pyright: ignore[reportGeneralTypeIssues] # Disallow unsafe get def get(self, __key: CaptureMethodEnum): """ @@ -128,11 +126,11 @@ def get(self, __key: CaptureMethodEnum): short_description="fast, most compatible, capped at 60fps", description=( f"\nOnly available in Windows 10.0.{WGC_MIN_BUILD} and up. " - f"\nDue to current technical limitations, Windows versions below 10.0.0.{LEARNING_MODE_DEVICE_BUILD}" - "\nrequire having at least one audio or video Capture Device connected and enabled." - "\nAllows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows. " - "\nAdds a yellow border on Windows 10 (not on Windows 11)." - "\nCaps at around 60 FPS. " + + f"\nDue to current technical limitations, Windows versions below 10.0.0.{LEARNING_MODE_DEVICE_BUILD}" + + "\nrequire having at least one audio or video Capture Device connected and enabled." + + "\nAllows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows. " + + "\nAdds a yellow border on Windows 10 (not on Windows 11)." + + "\nCaps at around 60 FPS. " ), implementation=WindowsGraphicsCaptureMethod, ) @@ -141,8 +139,8 @@ def get(self, __key: CaptureMethodEnum): short_description="fastest, least compatible", description=( "\nThe best option when compatible. But it cannot properly record " - "\nOpenGL, Hardware Accelerated or Exclusive Fullscreen windows. " - "\nThe smaller the selected region, the more efficient it is. " + + "\nOpenGL, Hardware Accelerated or Exclusive Fullscreen windows. " + + "\nThe smaller the selected region, the more efficient it is. " ), implementation=BitBltCaptureMethod, @@ -158,12 +156,12 @@ def get(self, __key: CaptureMethodEnum): short_description="slower, bound to display", description=( "\nDuplicates the desktop using Direct3D. " - "\nIt can record OpenGL and Hardware Accelerated windows. " - "\nAbout 10-15x slower than BitBlt. Not affected by window size. " - "\nOverlapping windows will show up and can't record across displays. " - "\nThis option may not be available for hybrid GPU laptops, " - "\nsee /docs/D3DDD-Note-Laptops.md for a solution. " - f"\nhttps://www.github.com/{GITHUB_REPOSITORY}#capture-method " + + "\nIt can record OpenGL and Hardware Accelerated windows. " + + "\nAbout 10-15x slower than BitBlt. Not affected by window size. " + + "\nOverlapping windows will show up and can't record across displays. " + + "\nThis option may not be available for hybrid GPU laptops, " + + "\nsee /docs/D3DDD-Note-Laptops.md for a solution. " + + f"\nhttps://www.github.com/{GITHUB_REPOSITORY}#capture-method " ), implementation=DesktopDuplicationCaptureMethod, ) @@ -172,9 +170,9 @@ def get(self, __key: CaptureMethodEnum): short_description="very slow, can affect rendering", description=( "\nUses BitBlt behind the scene, but passes a special flag " - "\nto PrintWindow to force rendering the entire desktop. " - "\nAbout 10-15x slower than BitBlt based on original window size " - "\nand can mess up some applications' rendering pipelines. " + + "\nto PrintWindow to force rendering the entire desktop. " + + "\nAbout 10-15x slower than BitBlt based on original window size " + + "\nand can mess up some applications' rendering pipelines. " ), implementation=ForceFullContentRenderingCaptureMethod, ) @@ -183,9 +181,9 @@ def get(self, __key: CaptureMethodEnum): short_description="see below", description=( "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " - "\nYou can select one below. " - "\nIf you want to use this with OBS' Virtual Camera, use the Virtualcam plugin instead " - "\nhttps://github.com/Avasam/obs-virtual-cam/releases" + + "\nYou can select one below. " + + "\nIf you want to use this with OBS' Virtual Camera, use the Virtualcam plugin instead " + + "\nhttps://github.com/Avasam/obs-virtual-cam/releases" ), implementation=VideoCaptureDeviceCaptureMethod, ) diff --git a/src/compare.py b/src/compare.py index 4faec749..2e197a73 100644 --- a/src/compare.py +++ b/src/compare.py @@ -6,13 +6,13 @@ import imagehash from PIL import Image -from utils import MAXBYTE, is_valid_image +from utils import MAXBYTE, RGBA_CHANNEL_COUNT, ColorChannel, is_valid_image MAXRANGE = MAXBYTE + 1 -CHANNELS = [0, 1, 2] +CHANNELS: list[int] = [ColorChannel.Red, ColorChannel.Green, ColorChannel.Blue] HISTOGRAM_SIZE = [8, 8, 8] RANGES = [0, MAXRANGE, 0, MAXRANGE, 0, MAXRANGE] -MASK_SIZE_MULTIPLIER = 3 * MAXBYTE * MAXBYTE +MASK_SIZE_MULTIPLIER = ColorChannel.Alpha * MAXBYTE * MAXBYTE def compare_histograms(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = None): @@ -20,12 +20,11 @@ def compare_histograms(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = Compares two images by calculating their histograms, normalizing them, and then comparing them using Bhattacharyya distance. - @param source: 3 color image of any given width and height - @param capture: An image matching the dimensions of the source + @param source: RGB or BGR image of any given width and height + @param capture: An image matching the shape, dimensions and format of the source @param mask: An image matching the dimensions of the source, but 1 channel grayscale @return: The similarity between the histograms as a number 0 to 1. """ - source_hist = cv2.calcHist([source], CHANNELS, mask, HISTOGRAM_SIZE, RANGES) capture_hist = cv2.calcHist([capture], CHANNELS, mask, HISTOGRAM_SIZE, RANGES) @@ -43,7 +42,6 @@ def compare_l2_norm(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = No @param mask: An image matching the dimensions of the source, but 1 channel grayscale @return: The similarity between the images as a number 0 to 1. """ - error = cv2.norm(source, capture, cv2.NORM_L2, mask) # The L2 Error is summed across all pixels, so this normalizes @@ -59,7 +57,7 @@ def compare_l2_norm(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = No def compare_template(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = None): """ Checks if the source is located within the capture by using the sum of square differences. - The mask is used to search for non-rectangular images within the capture + The mask is used to search for non-rectangular images within the capture. @param source: The subsection being searched for within the capture @param capture: Capture of an image larger than the source @@ -67,7 +65,6 @@ def compare_template(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = N @return: The best similarity for a region found in the image. This is represented as a number from 0 to 1. """ - result = cv2.matchTemplate(capture, source, cv2.TM_SQDIFF, mask=mask) min_val, *_ = cv2.minMaxLoc(result) @@ -89,7 +86,6 @@ def compare_phash(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = None @param mask: An image matching the dimensions of the source, but 1 channel grayscale @return: The similarity between the hashes of the image as a number 0 to 1. """ - # Since imagehash doesn't have any masking itself, bitwise_and will allow us # to apply the mask to the source and capture before calculating the pHash for # each of the images. As a result of this, this function is not going to be very @@ -107,13 +103,16 @@ def compare_phash(source: cv2.Mat, capture: cv2.Mat, mask: cv2.Mat | None = None def check_if_image_has_transparency(image: cv2.Mat): # Check if there's a transparency channel (4th channel) and if at least one pixel is transparent (< 255) - if image.shape[2] != 4: + if image.shape[2] != RGBA_CHANNEL_COUNT: return False - mean: float = image[:, :, 3].mean() + mean: float = image[:, :, ColorChannel.Alpha].mean() if mean == 0: # Non-transparent images code path is usually faster and simpler, so let's return that return False - # TODO error message if all pixels are transparent + # TODO: error message if all pixels are transparent # (the image appears as all black in windows, so it's not obvious for the user what they did wrong) - return mean != 255 + return mean != MAXBYTE + + +COMPARE_METHODS_BY_INDEX = {0: compare_l2_norm, 1: compare_histograms, 2: compare_phash} diff --git a/src/error_messages.py b/src/error_messages.py index 3aa4dd53..8eb6c9a0 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -1,4 +1,4 @@ -"""Error messages""" +"""Error messages.""" from __future__ import annotations import os diff --git a/src/hotkeys.py b/src/hotkeys.py index 3648416f..e0fe3c65 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -29,9 +29,7 @@ def remove_all_hotkeys(): def before_setting_hotkey(autosplit: AutoSplit): - """ - Do all of these after you click "Set Hotkey" but before you type the hotkey - """ + """Do all of these after you click "Set Hotkey" but before you type the hotkey.""" autosplit.start_auto_splitter_button.setEnabled(False) if autosplit.SettingsWidget: for hotkey in HOTKEYS: @@ -41,7 +39,7 @@ def before_setting_hotkey(autosplit: AutoSplit): def after_setting_hotkey(autosplit: AutoSplit): """ Do all of these things after you set a hotkey. - A signal connects to this because changing GUI stuff is only possible in the main thread + A signal connects to this because changing GUI stuff is only possible in the main thread. """ if not autosplit.is_running: autosplit.start_auto_splitter_button.setEnabled(True) @@ -78,9 +76,7 @@ def _unhook(hotkey_callback: Callable[[], None] | None): def _send_hotkey(hotkey_or_scan_code: int | str | None): - """ - Supports sending the appropriate scan code for all the special cases - """ + """Supports sending the appropriate scan code for all the special cases.""" if not hotkey_or_scan_code: return @@ -109,7 +105,7 @@ def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) NOTE: This is a workaround very specific to numpads. Windows reports different physical keys with the same scan code. For example, "Home", "Num Home" and "Num 7" are all `71`. - See: https://github.com/boppreh/keyboard/issues/171#issuecomment-390437684 + See: https://github.com/boppreh/keyboard/issues/171#issuecomment-390437684. Since we reuse the key string we set to send to LiveSplit, we can't use fake names like "num home". We're also trying to achieve the same hotkey behaviour as LiveSplit has. @@ -137,16 +133,14 @@ def _hotkey_action(keyboard_event: keyboard.KeyboardEvent, key_name: str, action """ We're doing the check here instead of saving the key code because the non-keypad shared keys are localized while the keypad ones aren't. - They also share scan codes on Windows + They also share scan codes on Windows. """ if keyboard_event.event_type == keyboard.KEY_DOWN and __validate_keypad(key_name, keyboard_event): action() def __get_key_name(keyboard_event: keyboard.KeyboardEvent): - """ - Ensures proper keypad name - """ + """Ensures proper keypad name.""" event_name = str(keyboard_event.name) # Normally this is done by keyboard.get_hotkey_name. But our code won't always get there. if event_name == "+": @@ -159,7 +153,7 @@ def __get_key_name(keyboard_event: keyboard.KeyboardEvent): def __get_hotkey_name(names: list[str]): """ Uses keyboard.get_hotkey_name but works with non-english modifiers and keypad - See: https://github.com/boppreh/keyboard/issues/516 + See: https://github.com/boppreh/keyboard/issues/516. """ def sorting_key(key: str): return not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) @@ -175,7 +169,7 @@ def sorting_key(key: str): def __read_hotkey(): """ Blocks until a hotkey combination is read. - Returns the hotkey_name and last KeyboardEvent + Returns the hotkey_name and last KeyboardEvent. """ names: list[str] = [] while True: @@ -281,7 +275,7 @@ def read_and_set_hotkey(): if autosplit.SettingsWidget: getattr(autosplit.SettingsWidget, f"{hotkey}_input").setText(hotkey_name) autosplit.settings_dict[f"{hotkey}_hotkey"] = hotkey_name # pyright: ignore[reportGeneralTypeIssues] - except Exception as exception: # pylint: disable=broad-except # We really want to catch everything here + except Exception as exception: # noqa: BLE001 # We really want to catch everything here error = exception autosplit.show_error_signal.emit(lambda: error_messages.exception_traceback(error)) finally: diff --git a/src/menu_bar.py b/src/menu_bar.py index fa6a3c6e..a37a9a75 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -12,20 +12,29 @@ import error_messages import user_profile from capture_method import ( - CAPTURE_METHODS, CameraInfo, CaptureMethodEnum, change_capture_method, get_all_video_capture_devices, + CAPTURE_METHODS, + CameraInfo, + CaptureMethodEnum, + change_capture_method, + get_all_video_capture_devices, ) from gen import about, design, resources_rc, settings as settings_ui, update_checker # noqa: F401 from hotkeys import HOTKEYS, Hotkey, set_hotkey from utils import ( - AUTOSPLIT_VERSION, FIRST_WIN_11_BUILD, GITHUB_REPOSITORY, WINDOWS_BUILD_NUMBER, decimal, fire_and_forget, + AUTOSPLIT_VERSION, + FIRST_WIN_11_BUILD, + GITHUB_REPOSITORY, + WINDOWS_BUILD_NUMBER, + decimal, + fire_and_forget, ) if TYPE_CHECKING: from AutoSplit import AutoSplit -class __AboutWidget(QtWidgets.QWidget, about.Ui_AboutAutoSplitWidget): - """About Window""" +class __AboutWidget(QtWidgets.QWidget, about.Ui_AboutAutoSplitWidget): # noqa: N801 # Private class + """About Window.""" def __init__(self): super().__init__() @@ -41,7 +50,7 @@ def open_about(autosplit: AutoSplit): autosplit.AboutWidget = __AboutWidget() -class __UpdateCheckerWidget(QtWidgets.QWidget, update_checker.Ui_UpdateChecker): +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): super().__init__() self.setupUi(self) @@ -82,7 +91,7 @@ def view_help(): webbrowser.open(f"https://github.com/{GITHUB_REPOSITORY}#tutorial") -class __CheckForUpdatesThread(QtCore.QThread): +class __CheckForUpdatesThread(QtCore.QThread): # noqa: N801 # Private class def __init__(self, autosplit: AutoSplit, check_on_open: bool): super().__init__() self.autosplit = autosplit @@ -112,7 +121,7 @@ def check_for_updates(autosplit: AutoSplit, check_on_open: bool = False): autosplit.CheckForUpdatesThread.start() -class __SettingsWidget(QtWidgets.QWidget, settings_ui.Ui_SettingsWidget): +class __SettingsWidget(QtWidgets.QWidget, settings_ui.Ui_SettingsWidget): # noqa: N801 # Private class __video_capture_devices: list[CameraInfo] = [] """ Used to temporarily store the existing cameras, @@ -136,9 +145,7 @@ def __set_value(self, key: str, value: Any): self.autosplit.settings_dict[key] = value def get_capture_device_index(self, capture_device_id: int): - """ - Returns 0 if the capture_device_id is invalid - """ + """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) except ValueError: diff --git a/src/region_selection.py b/src/region_selection.py index 7fd3e91b..c5e18dbe 100644 --- a/src/region_selection.py +++ b/src/region_selection.py @@ -10,6 +10,7 @@ import numpy as np from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtTest import QTest +from typing_extensions import override from win32 import win32gui from win32con import SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN from winsdk._winrt import initialize_with_window # pylint: disable=no-name-in-module @@ -17,14 +18,23 @@ from winsdk.windows.graphics.capture import GraphicsCaptureItem, GraphicsCapturePicker import error_messages -from utils import MAXBYTE, get_window_bounds, getTopWindowAt, is_valid_hwnd, is_valid_image +from utils import ( + MAXBYTE, + RGB_CHANNEL_COUNT, + ImageShape, + get_window_bounds, + getTopWindowAt, + is_valid_hwnd, + is_valid_image, +) user32 = ctypes.windll.user32 if TYPE_CHECKING: from AutoSplit import AutoSplit - +ALIGN_REGION_THRESHOLD = 0.9 +BORDER_WIDTH = 2 SUPPORTED_IMREAD_FORMATS = [ ("Windows bitmaps", "*.bmp *.dib"), ("JPEG files", "*.jpeg *.jpg *.jpe"), @@ -47,9 +57,7 @@ def __select_graphics_item(autosplit: AutoSplit): # pyright: ignore [reportUnusedFunction] # TODO: For later as a different picker option - """ - Uses the built-in GraphicsCapturePicker to select the Window - """ + """Uses the built-in GraphicsCapturePicker to select the Window.""" def callback(async_operation: IAsyncOperation[GraphicsCaptureItem], async_status: AsyncStatus): try: if async_status != AsyncStatus.COMPLETED: @@ -195,7 +203,7 @@ def align_region(autosplit: AutoSplit): # Go ahead and check if this satisfies our requirement before setting the region # We don't want a low similarity image to be aligned. - if best_match < 0.9: + if best_match < ALIGN_REGION_THRESHOLD: error_messages.alignment_not_matched() return @@ -221,14 +229,14 @@ def __set_region_values(autosplit: AutoSplit, left: int, top: int, width: int, h autosplit.height_spinbox.setValue(height) -def __test_alignment(capture: cv2.Mat, template: cv2.Mat): # pylint: disable=too-many-locals +def __test_alignment(capture: cv2.Mat, template: cv2.Mat): """ Obtain the best matching point for the template within the capture. This assumes that the template is actually smaller than the dimensions of the capture. Since we are using SQDIFF the best match will be the min_val which is located at min_loc. The best match found in the image, set everything to 0 by default - so that way the first match will overwrite these values + so that way the first match will overwrite these values. """ best_match = 0.0 best_height = 0 @@ -237,7 +245,7 @@ def __test_alignment(capture: cv2.Mat, template: cv2.Mat): # pylint: disable=to # Add alpha channel to template if it's missing. The cv2.matchTemplate() function # needs both images to have the same color dimensions, and capture has an alpha channel - if template.shape[2] == 3: + if template.shape[ImageShape.Channels] == RGB_CHANNEL_COUNT: template = cv2.cvtColor(template, cv2.COLOR_BGR2BGRA) # This tests 50 images scaled from 20% to 300% of the original template size @@ -287,9 +295,11 @@ class BaseSelectWidget(QtWidgets.QWidget): _x = 0 _y = 0 + @override def x(self): return self._x + @override def y(self): return self._y @@ -308,16 +318,16 @@ def __init__(self): self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) self.show() + @override def keyPressEvent(self, a0: QtGui.QKeyEvent): if a0.key() == QtCore.Qt.Key.Key_Escape: self.close() class SelectWindowWidget(BaseSelectWidget): - """ - Widget to select a window and obtain its bounds - """ + """Widget to select a window and obtain its bounds.""" + @override def mouseReleaseEvent(self, a0: QtGui.QMouseEvent): self._x = int(a0.position().x()) + self.geometry().x() self._y = int(a0.position().y()) + self.geometry().y() @@ -327,8 +337,9 @@ def mouseReleaseEvent(self, a0: QtGui.QMouseEvent): class SelectRegionWidget(BaseSelectWidget): """ Widget for dragging screen region - https://github.com/harupy/snipping-tool + https://github.com/harupy/snipping-tool. """ + _right: int = 0 _bottom: int = 0 __begin = QtCore.QPoint() @@ -338,28 +349,34 @@ def __init__(self): QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.CrossCursor)) super().__init__() + @override def height(self): return self._bottom - self._y + @override def width(self): return self._right - self._x + @override def paintEvent(self, a0: QtGui.QPaintEvent): if self.__begin != self.__end: qpainter = QtGui.QPainter(self) - qpainter.setPen(QtGui.QPen(QtGui.QColor("red"), 2)) + qpainter.setPen(QtGui.QPen(QtGui.QColor("red"), BORDER_WIDTH)) qpainter.setBrush(QtGui.QColor("opaque")) qpainter.drawRect(QtCore.QRect(self.__begin, self.__end)) + @override def mousePressEvent(self, a0: QtGui.QMouseEvent): self.__begin = a0.position().toPoint() self.__end = self.__begin self.update() + @override def mouseMoveEvent(self, a0: QtGui.QMouseEvent): self.__end = a0.position().toPoint() self.update() + @override def mouseReleaseEvent(self, a0: QtGui.QMouseEvent): if self.__begin != self.__end: # The coordinates are pulled relative to the top left of the set geometry, @@ -371,6 +388,7 @@ def mouseReleaseEvent(self, a0: QtGui.QMouseEvent): self.close() + @override def close(self): QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor)) return super().close() diff --git a/src/split_parser.py b/src/split_parser.py index 422e8abd..250c866d 100644 --- a/src/split_parser.py +++ b/src/split_parser.py @@ -28,13 +28,15 @@ def __value_from_filename( delimiters: str, default_value: T, ) -> T: - if len(delimiters) != 2: + if len(delimiters) != 2: # noqa: PLR2004 raise ValueError("delimiters parameter must contain exactly 2 characters") try: value_type = type(default_value) - return value_type(filename.split(delimiters[0], 1)[1].split(delimiters[1])[0]) + value = value_type(filename.split(delimiters[0], 1)[1].split(delimiters[1])[0]) except (IndexError, ValueError): return default_value + else: + return value def threshold_from_filename(filename: str): @@ -45,13 +47,12 @@ def threshold_from_filename(filename: str): @param filename: String containing the file's name @return: A valid threshold, if not then None """ - # Check to make sure there is a valid floating point number between # parentheses of the filename value = __value_from_filename(filename, "()", -1.0) # Check to make sure if it is a valid threshold - return value if 0.0 <= value <= 1.0 else None + return value if 0 <= value <= 1 else None def pause_from_filename(filename: str): @@ -62,13 +63,12 @@ def pause_from_filename(filename: str): @param filename: String containing the file's name @return: A valid pause time, if not then None """ - # Check to make sure there is a valid pause time between brackets # of the filename value = __value_from_filename(filename, "[]", -1.0) # Pause times should always be positive or zero - return value if value >= 0.0 else None + return value if value >= 0 else None def delay_time_from_filename(filename: str): @@ -79,7 +79,6 @@ def delay_time_from_filename(filename: str): @param filename: String containing the file's name @return: A valid delay time, if not then none """ - # Check to make sure there is a valid delay time between brackets # of the filename value = __value_from_filename(filename, "##", -1) @@ -96,7 +95,6 @@ def loop_from_filename(filename: str): @param filename: String containing the file's name @return: A valid loop number, if not then 1 """ - # Check to make sure there is a valid delay time between brackets # of the filename value = __value_from_filename(filename, "@@", 1) @@ -113,7 +111,6 @@ def comparison_method_from_filename(filename: str): @param filename: String containing the file's name @return: A valid comparison method index, if not then none """ - # Check to make sure there is a valid delay time between brackets # of the filename value = __value_from_filename(filename, "^^", -1) @@ -124,7 +121,7 @@ def comparison_method_from_filename(filename: str): def flags_from_filename(filename: str): """ - Retrieve the flags from the filename, if there are no flags then 0 is returned + Retrieve the flags from the filename, if there are no flags then 0 is returned. @param filename: String containing the file's name @return: The flags as an integer, if invalid flags are found it returns 0 @@ -134,7 +131,6 @@ def flags_from_filename(filename: str): "b" = below threshold, after threshold is met, split when it goes below the threhsold. "p" = pause, hit pause key when this split is found """ - # Check to make sure there are flags between curly braces # of the filename flags_str = __value_from_filename(filename, "{}", "") diff --git a/src/user_profile.py b/src/user_profile.py index b737a543..9be788d6 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -68,18 +68,14 @@ def have_settings_changed(autosplit: AutoSplit): def save_settings(autosplit: AutoSplit): - """ - @return: The save settings filepath. Or None if "Save Settings As" is cancelled - """ + """@return: The save settings filepath. Or None if "Save Settings As" is cancelled.""" return __save_settings_to_file(autosplit, autosplit.last_successfully_loaded_settings_file_path) \ if autosplit.last_successfully_loaded_settings_file_path \ else save_settings_as(autosplit) def save_settings_as(autosplit: AutoSplit): - """ - @return: The save settings filepath selected. Empty if cancelled - """ + """@return: The save settings filepath selected. Empty if cancelled.""" # User picks save destination save_settings_file_path = QtWidgets.QFileDialog.getSaveFileName( autosplit, @@ -110,7 +106,7 @@ def __load_settings_from_file(autosplit: AutoSplit, load_settings_file_path: str autosplit.show_error_signal.emit(error_messages.old_version_settings_file) return False try: - with open(load_settings_file_path, "r", encoding="utf-8") as file: + with open(load_settings_file_path, encoding="utf-8") as file: # Casting here just so we can build an actual UserProfileDict once we're done validating # Fallback to default settings if some are missing from the file. This happens when new settings are added. loaded_settings = cast( @@ -204,10 +200,7 @@ def load_check_for_updates_on_open(autosplit: AutoSplit): def set_check_for_updates_on_open(design_window: design.Ui_MainWindow, value: bool): - """ - Sets the "Check For Updates On Open" QSettings value and the checkbox state - """ - + """Sets the "Check For Updates On Open" QSettings value and the checkbox state.""" design_window.action_check_for_updates_on_open.setChecked(value) QtCore \ .QSettings("AutoSplit", "Check For Updates On Open") \ diff --git a/src/utils.py b/src/utils.py index beb96097..dd374a74 100644 --- a/src/utils.py +++ b/src/utils.py @@ -6,12 +6,14 @@ import os import sys from collections.abc import Callable, Iterable +from enum import IntEnum from platform import version from threading import Thread from typing import TYPE_CHECKING, Any, TypeVar, cast import cv2 import win32ui +from typing_extensions import TypeGuard from win32 import win32gui from winsdk.windows.ai.machinelearning import LearningModelDevice, LearningModelDeviceKind from winsdk.windows.media.capture import MediaCapture @@ -21,10 +23,26 @@ if TYPE_CHECKING: # Source does not exist, keep this under TYPE_CHECKING from _win32typing import PyCDC # pyright: ignore[reportMissingModuleSource] - from typing_extensions import ParamSpec, TypeGuard - P = ParamSpec("P") DWMWA_EXTENDED_FRAME_BOUNDS = 9 +MAXBYTE = 255 +RGB_CHANNEL_COUNT = 3 +"""How many channels in an RGB image""" +RGBA_CHANNEL_COUNT = 4 +"""How many channels in an RGB image""" + + +class ImageShape(IntEnum): + X = 0 + Y = 1 + Channels = 2 + + +class ColorChannel(IntEnum): + Red = 0 + Green = 1 + Blue = 2 + Alpha = 3 def decimal(value: int | float): @@ -33,13 +51,11 @@ def decimal(value: int | float): def is_digit(value: str | int | None): - """ - Checks if `value` is a single-digit string from 0-9 - """ + """Checks if `value` is a single-digit string from 0-9.""" if value is None: return False try: - return 0 <= int(value) <= 9 + return 0 <= int(value) <= 9 # noqa: PLR2004 except (ValueError, TypeError): return False @@ -49,7 +65,7 @@ def is_valid_image(image: cv2.Mat | None) -> TypeGuard[cv2.Mat]: def is_valid_hwnd(hwnd: int): - """Validate the hwnd points to a valid window and not the desktop or whatever window obtained with `\"\"`""" + """Validate the hwnd points to a valid window and not the desktop or whatever window obtained with `""`.""" if not hwnd: return False if sys.platform == "win32": @@ -62,7 +78,7 @@ def is_valid_hwnd(hwnd: int): def first(iterable: Iterable[T]) -> T: - """@return: The first element of a collection. Dictionaries will return the first key""" + """@return: The first element of a collection. Dictionaries will return the first key.""" return next(iter(iterable)) @@ -91,7 +107,7 @@ def get_window_bounds(hwnd: int) -> tuple[int, int, int, int]: def open_file(file_path: str | bytes | os.PathLike[str] | os.PathLike[bytes]): - os.startfile(file_path) # nosec B606 + os.startfile(file_path) # noqa: S606 def get_or_create_eventloop(): @@ -118,7 +134,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).direct3_d11_device # TODO: Unknown potential error, I don't have an older Win10 machine to test. - except BaseException: # pylint: disable=broad-except + except BaseException: # noqa: S110,BLE001 pass if not direct_3d_device: raise OSError("Unable to initialize a Direct3D Device.") @@ -134,7 +150,7 @@ def try_get_direct3d_device(): def fire_and_forget(func: Callable[..., Any]): """ - Runs synchronous function asynchronously without waiting for a response + Runs synchronous function asynchronously without waiting for a response. Uses threads on Windows because `RuntimeError: There is no current event loop in thread 'MainThread'.` @@ -172,14 +188,13 @@ def title(self): # Environment specifics -WINDOWS_BUILD_NUMBER = int(version().split(".")[2]) if sys.platform == "win32" else -1 +WINDOWS_BUILD_NUMBER = int(version().split(".")[-1]) if sys.platform == "win32" else -1 FIRST_WIN_11_BUILD = 22000 """AutoSplit Version number""" FROZEN = hasattr(sys, "frozen") """Running from build made by PyInstaller""" auto_split_directory = os.path.dirname(sys.executable if FROZEN else os.path.abspath(__file__)) """The directory of either the AutoSplit executable or AutoSplit.py""" -MAXBYTE = 255 # Shared strings # Check `excludeBuildNumber` during workflow dispatch build generate a clean version number diff --git a/typings/cv2/__init__.pyi b/typings/cv2/__init__.pyi index 6fb4b21c..3a7ad7f6 100644 --- a/typings/cv2/__init__.pyi +++ b/typings/cv2/__init__.pyi @@ -1,5 +1,10 @@ from cv2 import ( - Error as Error, data as data, gapi as gapi, mat_wrapper as mat_wrapper, misc as misc, utils as utils, + Error as Error, + data as data, + gapi as gapi, + mat_wrapper as mat_wrapper, + misc as misc, + utils as utils, version as version, ) from cv2.cv2 import * # noqa: F403 @@ -14,4 +19,4 @@ def bootstrap() -> None: ... Mat: TypeAlias = WrappedMat | _NDArray # TODO: Make Mat generic with int or float -_MatF: TypeAlias = WrappedMat | _NDArray # noqa: Y047 +_MatF: TypeAlias = WrappedMat | _NDArray diff --git a/typings/cv2/cv2.pyi b/typings/cv2/cv2.pyi index 0441df91..e46c09fe 100644 --- a/typings/cv2/cv2.pyi +++ b/typings/cv2/cv2.pyi @@ -17,7 +17,7 @@ _NumericScalar: TypeAlias = float | bool | None # cv::Scalar _Scalar: TypeAlias = Mat | _NumericScalar | Sequence[_NumericScalar] # cv::TermCriteria -_TermCriteria: TypeAlias = Union[tuple[int, int, float], Sequence[float]] # noqa: Y047 +_TermCriteria: TypeAlias = Union[tuple[int, int, float], Sequence[float]] # cv::Point _Point: TypeAlias = Union[tuple[int, int], Sequence[int]] # cv::Size @@ -31,9 +31,9 @@ _SizeFloat: TypeAlias = Union[tuple[float, float], Sequence[float]] # cv::Rect _Rect: TypeAlias = Union[tuple[int, int, int, int], Sequence[int]] # cv::Rect -_RectFloat: TypeAlias = Union[tuple[int, int, int, int], Sequence[int]] # noqa: Y047 +_RectFloat: TypeAlias = Union[tuple[int, int, int, int], Sequence[int]] # cv::RotatedRect -_RotatedRect: TypeAlias = Union[tuple[_PointFloat, _SizeFloat, float], Sequence[_PointFloat | _SizeFloat | float]] # noqa: Y047 +_RotatedRect: TypeAlias = Union[tuple[_PointFloat, _SizeFloat, float], Sequence[_PointFloat | _SizeFloat | float]] _RotatedRectResult: TypeAlias = tuple[tuple[float, float], tuple[float, float], float] # cv:UMat, cv::InputArray, cv::OutputArray and cv::InputOutputArray _UMat: TypeAlias = UMat | _MatF | _NumericScalar @@ -42,191 +42,191 @@ _UMat: TypeAlias = UMat | _MatF | _NumericScalar # These are temporary placeholder return types, as were in the docstrings signatures from microsoft/python-type-stubs # This is often (but not always) a sign that a TypeVar should be used to return the same type as a param. # retval is equivalent to Unknown -_flow: TypeAlias = Incomplete # noqa: Y042 -_image: TypeAlias = Incomplete # noqa: Y042 -_edgeList: TypeAlias = Incomplete # noqa: Y042 -_leadingEdgeList: TypeAlias = Incomplete # noqa: Y042 -_triangleList: TypeAlias = Incomplete # noqa: Y042 -_matches_info: TypeAlias = Incomplete # noqa: Y042 -_arg3: TypeAlias = Incomplete # noqa: Y042 -_outputBlobs: TypeAlias = Incomplete # noqa: Y042 -_layersTypes: TypeAlias = Incomplete # noqa: Y042 -_detections: TypeAlias = Incomplete # noqa: Y042 -_results: TypeAlias = Incomplete # noqa: Y042 -_corners: TypeAlias = Incomplete # noqa: Y042 -_pts: TypeAlias = Incomplete # noqa: Y042 -_dst: TypeAlias = Incomplete # noqa: Y042 -_markers: TypeAlias = Incomplete # noqa: Y042 -_masks: TypeAlias = Incomplete # noqa: Y042 -_window: TypeAlias = Incomplete # noqa: Y042 -_edges: TypeAlias = Incomplete # noqa: Y042 -_lowerBound: TypeAlias = Incomplete # noqa: Y042 -_circles: TypeAlias = Incomplete # noqa: Y042 -_lines: TypeAlias = Incomplete # noqa: Y042 -_hu: TypeAlias = Incomplete # noqa: Y042 -_points2f: TypeAlias = Incomplete # noqa: Y042 -_keypoints: TypeAlias = Incomplete # noqa: Y042 -_mean: TypeAlias = Incomplete # noqa: Y042 -_eigenvectors: TypeAlias = Incomplete # noqa: Y042 -_eigenvalues: TypeAlias = Incomplete # noqa: Y042 -_result: TypeAlias = Incomplete # noqa: Y042 -_mtxR: TypeAlias = Incomplete # noqa: Y042 -_mtxQ: TypeAlias = Incomplete # noqa: Y042 +_flow: TypeAlias = Incomplete +_image: TypeAlias = Incomplete +_edgeList: TypeAlias = Incomplete +_leadingEdgeList: TypeAlias = Incomplete +_triangleList: TypeAlias = Incomplete +_matches_info: TypeAlias = Incomplete +_arg3: TypeAlias = Incomplete +_outputBlobs: TypeAlias = Incomplete +_layersTypes: TypeAlias = Incomplete +_detections: TypeAlias = Incomplete +_results: TypeAlias = Incomplete +_corners: TypeAlias = Incomplete +_pts: TypeAlias = Incomplete +_dst: TypeAlias = Incomplete +_markers: TypeAlias = Incomplete +_masks: TypeAlias = Incomplete +_window: TypeAlias = Incomplete +_edges: TypeAlias = Incomplete +_lowerBound: TypeAlias = Incomplete +_circles: TypeAlias = Incomplete +_lines: TypeAlias = Incomplete +_hu: TypeAlias = Incomplete +_points2f: TypeAlias = Incomplete +_keypoints: TypeAlias = Incomplete +_mean: TypeAlias = Incomplete +_eigenvectors: TypeAlias = Incomplete +_eigenvalues: TypeAlias = Incomplete +_result: TypeAlias = Incomplete +_mtxR: TypeAlias = Incomplete +_mtxQ: TypeAlias = Incomplete _Qx: TypeAlias = Incomplete _Qy: TypeAlias = Incomplete _Qz: TypeAlias = Incomplete -_jacobian: TypeAlias = Incomplete # noqa: Y042 -_w: TypeAlias = Incomplete # noqa: Y042 -_u: TypeAlias = Incomplete # noqa: Y042 -_vt: TypeAlias = Incomplete # noqa: Y042 -_approxCurve: TypeAlias = Incomplete # noqa: Y042 -_img: TypeAlias = Incomplete # noqa: Y042 -_dist: TypeAlias = Incomplete # noqa: Y042 -_nidx: TypeAlias = Incomplete # noqa: Y042 -_points: TypeAlias = Incomplete # noqa: Y042 -_pyramid: TypeAlias = Incomplete # noqa: Y042 -_covar: TypeAlias = Incomplete # noqa: Y042 -_nextPts: TypeAlias = Incomplete # noqa: Y042 -_status: TypeAlias = Incomplete # noqa: Y042 -_err: TypeAlias = Incomplete # noqa: Y042 -_cameraMatrix: TypeAlias = Incomplete # noqa: Y042 -_distCoeffs: TypeAlias = Incomplete # noqa: Y042 -_rvecs: TypeAlias = Incomplete # noqa: Y042 -_tvecs: TypeAlias = Incomplete # noqa: Y042 -_stdDeviationsIntrinsics: TypeAlias = Incomplete # noqa: Y042 -_stdDeviationsExtrinsics: TypeAlias = Incomplete # noqa: Y042 -_perViewErrors: TypeAlias = Incomplete # noqa: Y042 -_newObjPoints: TypeAlias = Incomplete # noqa: Y042 -_stdDeviationsObjPoints: TypeAlias = Incomplete # noqa: Y042 +_jacobian: TypeAlias = Incomplete +_w: TypeAlias = Incomplete +_u: TypeAlias = Incomplete +_vt: TypeAlias = Incomplete +_approxCurve: TypeAlias = Incomplete +_img: TypeAlias = Incomplete +_dist: TypeAlias = Incomplete +_nidx: TypeAlias = Incomplete +_points: TypeAlias = Incomplete +_pyramid: TypeAlias = Incomplete +_covar: TypeAlias = Incomplete +_nextPts: TypeAlias = Incomplete +_status: TypeAlias = Incomplete +_err: TypeAlias = Incomplete +_cameraMatrix: TypeAlias = Incomplete +_distCoeffs: TypeAlias = Incomplete +_rvecs: TypeAlias = Incomplete +_tvecs: TypeAlias = Incomplete +_stdDeviationsIntrinsics: TypeAlias = Incomplete +_stdDeviationsExtrinsics: TypeAlias = Incomplete +_perViewErrors: TypeAlias = Incomplete +_newObjPoints: TypeAlias = Incomplete +_stdDeviationsObjPoints: TypeAlias = Incomplete _R_cam2gripper: TypeAlias = Incomplete -_t_cam2gripper: TypeAlias = Incomplete # noqa: Y042 -_fovx: TypeAlias = Incomplete # noqa: Y042 -_fovy: TypeAlias = Incomplete # noqa: Y042 -_focalLength: TypeAlias = Incomplete # noqa: Y042 -_principalPoint: TypeAlias = Incomplete # noqa: Y042 -_aspectRatio: TypeAlias = Incomplete # noqa: Y042 -_magnitude: TypeAlias = Incomplete # noqa: Y042 -_angle: TypeAlias = Incomplete # noqa: Y042 -_pt1: TypeAlias = Incomplete # noqa: Y042 -_pt2: TypeAlias = Incomplete # noqa: Y042 -_pos: TypeAlias = Incomplete # noqa: Y042 -_m: TypeAlias = Incomplete # noqa: Y042 -_rvec3: TypeAlias = Incomplete # noqa: Y042 -_tvec3: TypeAlias = Incomplete # noqa: Y042 -_dr3dr1: TypeAlias = Incomplete # noqa: Y042 -_dr3dt1: TypeAlias = Incomplete # noqa: Y042 -_dr3dr2: TypeAlias = Incomplete # noqa: Y042 -_dr3dt2: TypeAlias = Incomplete # noqa: Y042 -_dt3dr1: TypeAlias = Incomplete # noqa: Y042 -_dt3dt1: TypeAlias = Incomplete # noqa: Y042 -_dt3dr2: TypeAlias = Incomplete # noqa: Y042 -_dt3dt2: TypeAlias = Incomplete # noqa: Y042 -_labels: TypeAlias = Incomplete # noqa: Y042 -_stats: TypeAlias = Incomplete # noqa: Y042 -_centroids: TypeAlias = Incomplete # noqa: Y042 -_dstmap1: TypeAlias = Incomplete # noqa: Y042 -_dstmap2: TypeAlias = Incomplete # noqa: Y042 -_hull: TypeAlias = Incomplete # noqa: Y042 -_convexityDefects: TypeAlias = Incomplete # noqa: Y042 -_newPoints1: TypeAlias = Incomplete # noqa: Y042 -_newPoints2: TypeAlias = Incomplete # noqa: Y042 -_grayscale: TypeAlias = Incomplete # noqa: Y042 -_color_boost: TypeAlias = Incomplete # noqa: Y042 +_t_cam2gripper: TypeAlias = Incomplete +_fovx: TypeAlias = Incomplete +_fovy: TypeAlias = Incomplete +_focalLength: TypeAlias = Incomplete +_principalPoint: TypeAlias = Incomplete +_aspectRatio: TypeAlias = Incomplete +_magnitude: TypeAlias = Incomplete +_angle: TypeAlias = Incomplete +_pt1: TypeAlias = Incomplete +_pt2: TypeAlias = Incomplete +_pos: TypeAlias = Incomplete +_m: TypeAlias = Incomplete +_rvec3: TypeAlias = Incomplete +_tvec3: TypeAlias = Incomplete +_dr3dr1: TypeAlias = Incomplete +_dr3dt1: TypeAlias = Incomplete +_dr3dr2: TypeAlias = Incomplete +_dr3dt2: TypeAlias = Incomplete +_dt3dr1: TypeAlias = Incomplete +_dt3dt1: TypeAlias = Incomplete +_dt3dr2: TypeAlias = Incomplete +_dt3dt2: TypeAlias = Incomplete +_labels: TypeAlias = Incomplete +_stats: TypeAlias = Incomplete +_centroids: TypeAlias = Incomplete +_dstmap1: TypeAlias = Incomplete +_dstmap2: TypeAlias = Incomplete +_hull: TypeAlias = Incomplete +_convexityDefects: TypeAlias = Incomplete +_newPoints1: TypeAlias = Incomplete +_newPoints2: TypeAlias = Incomplete +_grayscale: TypeAlias = Incomplete +_color_boost: TypeAlias = Incomplete _R1: TypeAlias = Incomplete _R2: TypeAlias = Incomplete -_t: TypeAlias = Incomplete # noqa: Y042 -_rotations: TypeAlias = Incomplete # noqa: Y042 -_translations: TypeAlias = Incomplete # noqa: Y042 -_normals: TypeAlias = Incomplete # noqa: Y042 -_rotMatrix: TypeAlias = Incomplete # noqa: Y042 -_transVect: TypeAlias = Incomplete # noqa: Y042 -_rotMatrixX: TypeAlias = Incomplete # noqa: Y042 -_rotMatrixY: TypeAlias = Incomplete # noqa: Y042 -_rotMatrixZ: TypeAlias = Incomplete # noqa: Y042 -_eulerAngles: TypeAlias = Incomplete # noqa: Y042 -_outImage: TypeAlias = Incomplete # noqa: Y042 -_outImg: TypeAlias = Incomplete # noqa: Y042 -_inliers: TypeAlias = Incomplete # noqa: Y042 -_out: TypeAlias = Incomplete # noqa: Y042 -_sharpness: TypeAlias = Incomplete # noqa: Y042 -_possibleSolutions: TypeAlias = Incomplete # noqa: Y042 -_buf: TypeAlias = Incomplete # noqa: Y042 -_meta: TypeAlias = Incomplete # noqa: Y042 -_centers: TypeAlias = Incomplete # noqa: Y042 -_contours: TypeAlias = Incomplete # noqa: Y042 -_hierarchy: TypeAlias = Incomplete # noqa: Y042 -_mask: TypeAlias = Incomplete # noqa: Y042 -_idx: TypeAlias = Incomplete # noqa: Y042 -_warpMatrix: TypeAlias = Incomplete # noqa: Y042 -_line: TypeAlias = Incomplete # noqa: Y042 -_rect: TypeAlias = Incomplete # noqa: Y042 -_kx: TypeAlias = Incomplete # noqa: Y042 -_ky: TypeAlias = Incomplete # noqa: Y042 -_validPixROI: TypeAlias = Incomplete # noqa: Y042 -_patch: TypeAlias = Incomplete # noqa: Y042 -_baseLine: TypeAlias = Incomplete # noqa: Y042 -_bgdModel: TypeAlias = Incomplete # noqa: Y042 -_fgdModel: TypeAlias = Incomplete # noqa: Y042 -_rectList: TypeAlias = Incomplete # noqa: Y042 -_weights: TypeAlias = Incomplete # noqa: Y042 -_mats: TypeAlias = Incomplete # noqa: Y042 -_map1: TypeAlias = Incomplete # noqa: Y042 -_map2: TypeAlias = Incomplete # noqa: Y042 -_sum: TypeAlias = Incomplete # noqa: Y042 -_sqsum: TypeAlias = Incomplete # noqa: Y042 -_tilted: TypeAlias = Incomplete # noqa: Y042 -_p12: TypeAlias = Incomplete # noqa: Y042 -_iM: TypeAlias = Incomplete # noqa: Y042 -_bestLabels: TypeAlias = Incomplete # noqa: Y042 -_dABdA: TypeAlias = Incomplete # noqa: Y042 -_dABdB: TypeAlias = Incomplete # noqa: Y042 -_stddev: TypeAlias = Incomplete # noqa: Y042 -_center: TypeAlias = Incomplete # noqa: Y042 -_radius: TypeAlias = Incomplete # noqa: Y042 -_triangle: TypeAlias = Incomplete # noqa: Y042 -_c: TypeAlias = Incomplete # noqa: Y042 -_a: TypeAlias = Incomplete # noqa: Y042 -_dst1: TypeAlias = Incomplete # noqa: Y042 -_dst2: TypeAlias = Incomplete # noqa: Y042 -_response: TypeAlias = Incomplete # noqa: Y042 -_x: TypeAlias = Incomplete # noqa: Y042 -_y: TypeAlias = Incomplete # noqa: Y042 -_imagePoints: TypeAlias = Incomplete # noqa: Y042 +_t: TypeAlias = Incomplete +_rotations: TypeAlias = Incomplete +_translations: TypeAlias = Incomplete +_normals: TypeAlias = Incomplete +_rotMatrix: TypeAlias = Incomplete +_transVect: TypeAlias = Incomplete +_rotMatrixX: TypeAlias = Incomplete +_rotMatrixY: TypeAlias = Incomplete +_rotMatrixZ: TypeAlias = Incomplete +_eulerAngles: TypeAlias = Incomplete +_outImage: TypeAlias = Incomplete +_outImg: TypeAlias = Incomplete +_inliers: TypeAlias = Incomplete +_out: TypeAlias = Incomplete +_sharpness: TypeAlias = Incomplete +_possibleSolutions: TypeAlias = Incomplete +_buf: TypeAlias = Incomplete +_meta: TypeAlias = Incomplete +_centers: TypeAlias = Incomplete +_contours: TypeAlias = Incomplete +_hierarchy: TypeAlias = Incomplete +_mask: TypeAlias = Incomplete +_idx: TypeAlias = Incomplete +_warpMatrix: TypeAlias = Incomplete +_line: TypeAlias = Incomplete +_rect: TypeAlias = Incomplete +_kx: TypeAlias = Incomplete +_ky: TypeAlias = Incomplete +_validPixROI: TypeAlias = Incomplete +_patch: TypeAlias = Incomplete +_baseLine: TypeAlias = Incomplete +_bgdModel: TypeAlias = Incomplete +_fgdModel: TypeAlias = Incomplete +_rectList: TypeAlias = Incomplete +_weights: TypeAlias = Incomplete +_mats: TypeAlias = Incomplete +_map1: TypeAlias = Incomplete +_map2: TypeAlias = Incomplete +_sum: TypeAlias = Incomplete +_sqsum: TypeAlias = Incomplete +_tilted: TypeAlias = Incomplete +_p12: TypeAlias = Incomplete +_iM: TypeAlias = Incomplete +_bestLabels: TypeAlias = Incomplete +_dABdA: TypeAlias = Incomplete +_dABdB: TypeAlias = Incomplete +_stddev: TypeAlias = Incomplete +_center: TypeAlias = Incomplete +_radius: TypeAlias = Incomplete +_triangle: TypeAlias = Incomplete +_c: TypeAlias = Incomplete +_a: TypeAlias = Incomplete +_dst1: TypeAlias = Incomplete +_dst2: TypeAlias = Incomplete +_response: TypeAlias = Incomplete +_x: TypeAlias = Incomplete +_y: TypeAlias = Incomplete +_imagePoints: TypeAlias = Incomplete _R: TypeAlias = Incomplete _R3: TypeAlias = Incomplete _P1: TypeAlias = Incomplete _P2: TypeAlias = Incomplete _P3: TypeAlias = Incomplete _Q: TypeAlias = Incomplete -_roi1: TypeAlias = Incomplete # noqa: Y042 -_roi2: TypeAlias = Incomplete # noqa: Y042 +_roi1: TypeAlias = Incomplete +_roi2: TypeAlias = Incomplete _3dImage: TypeAlias = Incomplete -_intersectingRegion: TypeAlias = Incomplete # noqa: Y042 -_blend: TypeAlias = Incomplete # noqa: Y042 -_boundingBoxes: TypeAlias = Incomplete # noqa: Y042 -_mtx: TypeAlias = Incomplete # noqa: Y042 -_roots: TypeAlias = Incomplete # noqa: Y042 -_z: TypeAlias = Incomplete # noqa: Y042 -_rvec: TypeAlias = Incomplete # noqa: Y042 -_tvec: TypeAlias = Incomplete # noqa: Y042 -_reprojectionError: TypeAlias = Incomplete # noqa: Y042 -_dx: TypeAlias = Incomplete # noqa: Y042 -_dy: TypeAlias = Incomplete # noqa: Y042 -_mv: TypeAlias = Incomplete # noqa: Y042 -_cameraMatrix1: TypeAlias = Incomplete # noqa: Y042 -_distCoeffs1: TypeAlias = Incomplete # noqa: Y042 -_cameraMatrix2: TypeAlias = Incomplete # noqa: Y042 -_distCoeffs2: TypeAlias = Incomplete # noqa: Y042 +_intersectingRegion: TypeAlias = Incomplete +_blend: TypeAlias = Incomplete +_boundingBoxes: TypeAlias = Incomplete +_mtx: TypeAlias = Incomplete +_roots: TypeAlias = Incomplete +_z: TypeAlias = Incomplete +_rvec: TypeAlias = Incomplete +_tvec: TypeAlias = Incomplete +_reprojectionError: TypeAlias = Incomplete +_dx: TypeAlias = Incomplete +_dy: TypeAlias = Incomplete +_mv: TypeAlias = Incomplete +_cameraMatrix1: TypeAlias = Incomplete +_distCoeffs1: TypeAlias = Incomplete +_cameraMatrix2: TypeAlias = Incomplete +_distCoeffs2: TypeAlias = Incomplete _T: TypeAlias = Incomplete _E: TypeAlias = Incomplete _F: TypeAlias = Incomplete -_validPixROI1: TypeAlias = Incomplete # noqa: Y042 -_validPixROI2: TypeAlias = Incomplete # noqa: Y042 +_validPixROI1: TypeAlias = Incomplete +_validPixROI2: TypeAlias = Incomplete _H1: TypeAlias = Incomplete _H2: TypeAlias = Incomplete -_points4D: TypeAlias = Incomplete # noqa: Y042 -_disparity: TypeAlias = Incomplete # noqa: Y042 -_triangulatedPoints: TypeAlias = Incomplete # noqa: Y042 +_points4D: TypeAlias = Incomplete +_disparity: TypeAlias = Incomplete +_triangulatedPoints: TypeAlias = Incomplete __version__: str diff --git a/typings/cv2/gapi/streaming.pyi b/typings/cv2/gapi/streaming.pyi index c330960f..2c49b006 100644 --- a/typings/cv2/gapi/streaming.pyi +++ b/typings/cv2/gapi/streaming.pyi @@ -1,11 +1,12 @@ from cv2.cv2 import GMat, GOpaqueT, gapi_streaming_queue_capacity +from typing_extensions import TypeAlias SYNC_POLICY_DONT_SYNC: int SYNC_POLICY_DROP: int sync_policy_dont_sync: int sync_policy_drop: int -queue_capacity = gapi_streaming_queue_capacity +queue_capacity: TypeAlias = gapi_streaming_queue_capacity def desync(g: GMat) -> GMat: ... diff --git a/typings/cv2/mat_wrapper/__init__.pyi b/typings/cv2/mat_wrapper/__init__.pyi index 7507add6..15d667e9 100644 --- a/typings/cv2/mat_wrapper/__init__.pyi +++ b/typings/cv2/mat_wrapper/__init__.pyi @@ -1,4 +1,6 @@ -import numpy +from __future__ import annotations + +import numpy as np from typing_extensions import TypeAlias _Unused: TypeAlias = object @@ -6,7 +8,7 @@ _Unused: TypeAlias = object __all__: list[str] = [] -_NDArray = numpy.ndarray[float, numpy.dtype[numpy.generic]] +_NDArray: TypeAlias = np.ndarray[float, np.dtype[np.generic]] # TODO: Make Mat generic with int or float