diff --git a/.editorconfig b/.editorconfig index a9b080f1..cd5c1f1c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,14 +1,14 @@ -root = true - -[*] -end_of_line = crlf -insert_final_newline = true -charset = utf-8 -indent_style = space -indent_size = 2 - -[*.py] -indent_size = 4 - -[*.ui] -indent_size = 1 +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 + +[*.ui] +indent_size = 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c0023fe..5d64c44d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,38 +1,38 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: pretty-format-json - exclude: ".vscode/.*" # Exclude jsonc - args: [--autofix, --no-sort-keys] - - id: trailing-whitespace - args: [--markdown-linebreak-ext=md] - - id: end-of-file-fixer - - id: mixed-line-ending - args: [--fix=crlf] - - id: check-case-conflict - - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.10.0 - hooks: - - id: pretty-format-ini - args: [--autofix] - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.1.1" # Must match requirements-dev.txt - hooks: - - id: ruff - args: [--fix] - - repo: https://github.com/hhatto/autopep8 - rev: "v2.0.4" # Must match requirements-dev.txt - hooks: - - id: autopep8 - - repo: https://github.com/asottile/add-trailing-comma - rev: v3.1.0 # Must match requirements-dev.txt - hooks: - - id: add-trailing-comma - -ci: - autoupdate_branch: dev - skip: - # Ignore until Linux support. We don't want lf everywhere yet - # And crlf fails on CI because pre-commit runs on linux - - "mixed-line-ending" +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: pretty-format-json + exclude: ".vscode/.*" # Exclude jsonc + args: [--autofix, --no-sort-keys] + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=crlf] + - id: check-case-conflict + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.10.0 + hooks: + - id: pretty-format-ini + args: [--autofix] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.1.1" # Must match requirements-dev.txt + hooks: + - id: ruff + args: [--fix] + - repo: https://github.com/hhatto/autopep8 + rev: "v2.0.4" # Must match requirements-dev.txt + hooks: + - id: autopep8 + - repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 # Must match requirements-dev.txt + hooks: + - id: add-trailing-comma + +ci: + autoupdate_branch: dev + skip: + # Ignore until Linux support. We don't want lf everywhere yet + # And crlf fails on CI because pre-commit runs on linux + - "mixed-line-ending" diff --git a/.vscode/settings.json b/.vscode/settings.json index 9ea48ddb..e6590170 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,148 +1,149 @@ -{ - "editor.rulers": [ - 80, - 120 - ], - "[git-commit]": { - "editor.rulers": [ - 72 - ] - }, - "[markdown]": { - "files.trimTrailingWhitespace": false, - }, - "files.trimTrailingWhitespace": true, - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "editor.comments.insertSpace": true, - "editor.insertSpaces": true, - "editor.detectIndentation": false, - "editor.tabSize": 2, - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": true, - // Let dedicated linter (Ruff) organize imports - "source.organizeImports": false, - }, - "emeraldwalk.runonsave": { - "commands": [ - { - "match": "\\.pyi?", - "cmd": "add-trailing-comma ${file}" - }, - ] - }, - "files.associations": { - ".flake8": "properties", - "*.qrc": "xml", - "*.ui": "xml" - }, - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/.*_cache": true, // mypy and Ruff cache - "**/__pycache__": true, - // Only show useful PyInstaller logs - "build/*.*": true, - "build/[b-z]*": true, - "build/**/localpycs": true, - "build/**/Tree-*": true, - "build/**/*.{manifest,pkg,zip,tcl,res,pyz}": true, - }, - "search.exclude": { - "**/*.code-search": true, - "*.lock": true, - }, - // Set the default formatter to help avoid Prettier - "[json][jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features", - }, - "[python]": { - // Ruff as a formatter doesn't fully satisfy our needs yet: https://github.com/astral-sh/ruff/discussions/7310 - "editor.defaultFormatter": "ms-python.autopep8", - "editor.tabSize": 4, - "editor.rulers": [ - 72, // PEP8-17 docstrings - // 79, // PEP8-17 default max - // 88, // Black default - // 99, // PEP8-17 acceptable max - 120, // Our hard rule - ], - }, - "mypy-type-checker.importStrategy": "fromEnvironment", - "mypy-type-checker.args": [ - // https://github.com/microsoft/vscode-mypy/issues/37#issuecomment-1602702174 - "--config-file=mypy.ini", - ], - "python.terminal.activateEnvironment": true, - // python.analysis is Pylance (pyright) configurations - "python.analysis.fixAll": [ - "source.convertImportFormat" - // Explicitly omiting "source.unusedImports", can be annoying when commenting code for debugging - ], - // Important to follow the config in pyrightconfig.json - "python.analysis.useLibraryCodeForTypes": false, - "python.analysis.diagnosticMode": "workspace", - "ruff.importStrategy": "fromEnvironment", - // Use the Ruff extension instead - "isort.check": false, - // linting/formatting options deprecated, use dedicated extensions instead - // https://github.com/microsoft/vscode-python/wiki/Migration-to-Python-Tools-Extensions - "python.linting.enabled": false, - "python.linting.banditEnabled": false, - "python.linting.flake8Enabled": false, - "python.linting.prospectorEnabled": false, - "python.linting.pycodestyleEnabled": false, - "python.linting.pylamaEnabled": false, - "python.linting.pylintEnabled": false, - "python.linting.mypyEnabled": false, - "python.formatting.provider": "none", - "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", - "powershell.codeFormatting.autoCorrectAliases": true, - "powershell.codeFormatting.trimWhitespaceAroundPipe": true, - "powershell.codeFormatting.useConstantStrings": true, - "powershell.codeFormatting.useCorrectCasing": true, - "powershell.codeFormatting.whitespaceBetweenParameters": true, - "powershell.integratedConsole.showOnStartup": false, - "terminal.integrated.defaultProfile.windows": "PowerShell", - "xml.codeLens.enabled": true, - "xml.format.spaceBeforeEmptyCloseTag": false, - "xml.format.preserveSpace": [ - // Default - "xsl:text", - "xsl:comment", - "xsl:processing-instruction", - "literallayout", - "programlisting", - "screen", - "synopsis", - "pre", - "xd:pre", - // Custom - "string" - ], - "[toml]": { - "editor.defaultFormatter": "tamasfe.even-better-toml" - }, - "evenBetterToml.formatter.alignComments": false, - "evenBetterToml.formatter.alignEntries": false, - "evenBetterToml.formatter.allowedBlankLines": 1, - "evenBetterToml.formatter.arrayAutoCollapse": true, - "evenBetterToml.formatter.arrayAutoExpand": true, - "evenBetterToml.formatter.arrayTrailingComma": true, - "evenBetterToml.formatter.columnWidth": 80, - "evenBetterToml.formatter.compactArrays": true, - "evenBetterToml.formatter.compactEntries": false, - "evenBetterToml.formatter.compactInlineTables": false, - "evenBetterToml.formatter.indentEntries": false, - "evenBetterToml.formatter.indentTables": false, - "evenBetterToml.formatter.inlineTableExpand": false, - "evenBetterToml.formatter.reorderArrays": true, - "evenBetterToml.formatter.trailingNewline": true, - // We like keeping TOML keys in a certain non-alphabetical order that feels more natural - "evenBetterToml.formatter.reorderKeys": false -} +{ + "editor.rulers": [ + 80, + 120 + ], + "[git-commit]": { + "editor.rulers": [ + 72 + ] + }, + "[markdown]": { + "files.trimTrailingWhitespace": false, + }, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.eol": "\n", + "editor.comments.insertSpace": true, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "editor.tabSize": 2, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true, + // Let dedicated linter (Ruff) organize imports + "source.organizeImports": false, + }, + "emeraldwalk.runonsave": { + "commands": [ + { + "match": "\\.pyi?", + "cmd": "add-trailing-comma ${file}" + }, + ] + }, + "files.associations": { + ".flake8": "properties", + "*.qrc": "xml", + "*.ui": "xml" + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.*_cache": true, // mypy and Ruff cache + "**/__pycache__": true, + // Only show useful PyInstaller logs + "build/*.*": true, + "build/[b-z]*": true, + "build/**/localpycs": true, + "build/**/Tree-*": true, + "build/**/*.{manifest,pkg,zip,tcl,res,pyz}": true, + }, + "search.exclude": { + "**/*.code-search": true, + "*.lock": true, + }, + // Set the default formatter to help avoid Prettier + "[json][jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features", + }, + "[python]": { + // Ruff as a formatter doesn't fully satisfy our needs yet: https://github.com/astral-sh/ruff/discussions/7310 + "editor.defaultFormatter": "ms-python.autopep8", + "editor.tabSize": 4, + "editor.rulers": [ + 72, // PEP8-17 docstrings + // 79, // PEP8-17 default max + // 88, // Black default + // 99, // PEP8-17 acceptable max + 120, // Our hard rule + ], + }, + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.args": [ + // https://github.com/microsoft/vscode-mypy/issues/37#issuecomment-1602702174 + "--config-file=mypy.ini", + ], + "python.terminal.activateEnvironment": true, + // python.analysis is Pylance (pyright) configurations + "python.analysis.fixAll": [ + "source.convertImportFormat" + // Explicitly omiting "source.unusedImports", can be annoying when commenting code for debugging + ], + // Important to follow the config in pyrightconfig.json + "python.analysis.useLibraryCodeForTypes": false, + "python.analysis.diagnosticMode": "workspace", + "ruff.importStrategy": "fromEnvironment", + // Use the Ruff extension instead + "isort.check": false, + // linting/formatting options deprecated, use dedicated extensions instead + // https://github.com/microsoft/vscode-python/wiki/Migration-to-Python-Tools-Extensions + "python.linting.enabled": false, + "python.linting.banditEnabled": false, + "python.linting.flake8Enabled": false, + "python.linting.prospectorEnabled": false, + "python.linting.pycodestyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.pylintEnabled": false, + "python.linting.mypyEnabled": false, + "python.formatting.provider": "none", + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", + "powershell.codeFormatting.autoCorrectAliases": true, + "powershell.codeFormatting.trimWhitespaceAroundPipe": true, + "powershell.codeFormatting.useConstantStrings": true, + "powershell.codeFormatting.useCorrectCasing": true, + "powershell.codeFormatting.whitespaceBetweenParameters": true, + "powershell.integratedConsole.showOnStartup": false, + "terminal.integrated.defaultProfile.windows": "PowerShell", + "xml.codeLens.enabled": true, + "xml.format.spaceBeforeEmptyCloseTag": false, + "xml.format.preserveSpace": [ + // Default + "xsl:text", + "xsl:comment", + "xsl:processing-instruction", + "literallayout", + "programlisting", + "screen", + "synopsis", + "pre", + "xd:pre", + // Custom + "string" + ], + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + }, + "evenBetterToml.formatter.alignComments": false, + "evenBetterToml.formatter.alignEntries": false, + "evenBetterToml.formatter.allowedBlankLines": 1, + "evenBetterToml.formatter.arrayAutoCollapse": true, + "evenBetterToml.formatter.arrayAutoExpand": true, + "evenBetterToml.formatter.arrayTrailingComma": true, + "evenBetterToml.formatter.columnWidth": 80, + "evenBetterToml.formatter.compactArrays": true, + "evenBetterToml.formatter.compactEntries": false, + "evenBetterToml.formatter.compactInlineTables": false, + "evenBetterToml.formatter.indentEntries": false, + "evenBetterToml.formatter.indentTables": false, + "evenBetterToml.formatter.inlineTableExpand": false, + "evenBetterToml.formatter.reorderArrays": true, + "evenBetterToml.formatter.trailingNewline": true, + // We like keeping TOML keys in a certain non-alphabetical order that feels more natural + "evenBetterToml.formatter.reorderKeys": false +} diff --git a/pyproject.toml b/pyproject.toml index 6eba3768..0e872573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,178 +1,178 @@ -# https://docs.astral.sh/ruff/configuration/ -[tool.ruff] -target-version = "py39" -line-length = 120 -select = ["ALL"] -preview = true -# https://docs.astral.sh/ruff/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 - "PGH003", # blanket-type-ignore - "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 - - ### - # These should be warnings (https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774) - ### - "FIX", # flake8-fixme - # Not all TODOs are worth an issue, this would be better as a warning - "TD003", # flake8-todos: missing-todo-link - - # False-positives - "TCH004", # https://github.com/astral-sh/ruff/issues/3821 - - ### - # Specific to this project - ### - "D205", # Not all docstrings have a short description + desrciption - # We have some Pascal case module names - "N999", # pep8-naming: Invalid module name - # Print are used as debug logs - "T20", # flake8-print - # This is a relatively small, low contributors project. Git blame suffice. - "TD002", # missing-todo-author - # Python 3.11, introduced "zero cost" exception handling - "PERF203", # try-except-in-loop - - ### FIXME/TODO (no warnings in Ruff yet: https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774): - "CPY001", # flake8-copyright - "PTH", # flake8-use-pathlib - # Ignore until linux support - "EXE", # flake8-executable -] - -[tool.ruff.per-file-ignores] -"typings/**/*.pyi" = [ - "F811", # Re-exports false positives - "F821", # https://github.com/astral-sh/ruff/issues/3011 - # The following can't be controlled for external libraries: - "A", # Shadowing builtin names - "ICN001", # unconventional-import-alias - "N8", # Naming conventions - "PLR0904", # Too many public methods - "PLR0913", # Argument count - "PLW3201", # misspelled dunder method name - "PYI042", # CamelCase TypeAlias -] - -# https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat -[tool.ruff.flake8-implicit-str-concat] -allow-multiline = false - -# https://docs.astral.sh/ruff/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/astral-sh/ruff/issues/3115 -known-local-folder = [ - "AutoControlledThread", - "AutoSplit", - "AutoSplitImage", - "capture_method", - "compare", - "error_messages", - "gen", - "hotkeys", - "menu_bar", - "region_selection", - "split_parser", - "user_profile", - "utils", -] - -# https://docs.astral.sh/ruff/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] -max_line_length = 120 -aggressive = 3 -exclude = ".venv/*,src/gen/*" -ignore = [ - "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma) - "E70", # Allow ... on same line as def - # Autofixed by Ruff - # Check for the "Fix" flag https://docs.astral.sh/ruff/rules/#pycodestyle-e-w - "E20", # whitespace-after-* & whitespace-before-* - "E211", # whitespace-before-parameters - "E231", # missing-whitespace - "E401", # I001: unsorted-imports - "E71", # Comparisons - "E731", # lambda-assignment - "W29", # Whitespaces - "W605", # invalid-escape-sequence -] - -# https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file -[tool.pyright] -typeCheckingMode = "strict" -# Prefer `pyright: ignore` -enableTypeIgnoreComments = false -# Extra strict -reportImplicitOverride = "error" -reportImplicitStringConcatenation = "error" -reportCallInDefaultInitializer = "error" -reportMissingSuperCall = "none" # False positives on base classes -reportPropertyTypeMismatch = "error" -reportUninitializedInstanceVariable = "error" -reportUnnecessaryTypeIgnoreComment = "error" -# Exclude from scanning when running pyright -exclude = [ - ".venv/", - # Auto generated, fails some strict pyright checks - "build/", - "src/gen/", -] -# Ignore must be specified for Pylance to stop displaying errors -ignore = [ - # We expect stub files to be incomplete or contain useless statements - "**/*.pyi", -] -reportUnusedCallResult = "none" -# Type stubs may not be completable -reportMissingTypeStubs = "warning" -# False positives with TYPE_CHECKING -reportImportCycles = "information" -# False positives with PySide .connect -reportFunctionMemberAccess = "none" -# Extra runtime safety -reportUnnecessaryComparison = "warning" -# Using Flake8/Ruff instead -reportUnusedImport = "none" -# numpy has way too many complex types that triggers this -reportUnknownMemberType = "none" +# https://docs.astral.sh/ruff/configuration/ +[tool.ruff] +target-version = "py39" +line-length = 120 +select = ["ALL"] +preview = true +# https://docs.astral.sh/ruff/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 + "PGH003", # blanket-type-ignore + "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 + + ### + # These should be warnings (https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774) + ### + "FIX", # flake8-fixme + # Not all TODOs are worth an issue, this would be better as a warning + "TD003", # flake8-todos: missing-todo-link + + # False-positives + "TCH004", # https://github.com/astral-sh/ruff/issues/3821 + + ### + # Specific to this project + ### + "D205", # Not all docstrings have a short description + desrciption + # We have some Pascal case module names + "N999", # pep8-naming: Invalid module name + # Print are used as debug logs + "T20", # flake8-print + # This is a relatively small, low contributors project. Git blame suffice. + "TD002", # missing-todo-author + # Python 3.11, introduced "zero cost" exception handling + "PERF203", # try-except-in-loop + + ### FIXME/TODO (no warnings in Ruff yet: https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774): + "CPY001", # flake8-copyright + "PTH", # flake8-use-pathlib + # Ignore until linux support + "EXE", # flake8-executable +] + +[tool.ruff.per-file-ignores] +"typings/**/*.pyi" = [ + "F811", # Re-exports false positives + "F821", # https://github.com/astral-sh/ruff/issues/3011 + # The following can't be controlled for external libraries: + "A", # Shadowing builtin names + "ICN001", # unconventional-import-alias + "N8", # Naming conventions + "PLR0904", # Too many public methods + "PLR0913", # Argument count + "PLW3201", # misspelled dunder method name + "PYI042", # CamelCase TypeAlias +] + +# https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat +[tool.ruff.flake8-implicit-str-concat] +allow-multiline = false + +# https://docs.astral.sh/ruff/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/astral-sh/ruff/issues/3115 +known-local-folder = [ + "AutoControlledThread", + "AutoSplit", + "AutoSplitImage", + "capture_method", + "compare", + "error_messages", + "gen", + "hotkeys", + "menu_bar", + "region_selection", + "split_parser", + "user_profile", + "utils", +] + +# https://docs.astral.sh/ruff/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] +max_line_length = 120 +aggressive = 3 +exclude = ".venv/*,src/gen/*" +ignore = [ + "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma) + "E70", # Allow ... on same line as def + # Autofixed by Ruff + # Check for the "Fix" flag https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "E20", # whitespace-after-* & whitespace-before-* + "E211", # whitespace-before-parameters + "E231", # missing-whitespace + "E401", # I001: unsorted-imports + "E71", # Comparisons + "E731", # lambda-assignment + "W29", # Whitespaces + "W605", # invalid-escape-sequence +] + +# https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file +[tool.pyright] +typeCheckingMode = "strict" +# Prefer `pyright: ignore` +enableTypeIgnoreComments = false +# Extra strict +reportImplicitOverride = "error" +reportImplicitStringConcatenation = "error" +reportCallInDefaultInitializer = "error" +reportMissingSuperCall = "none" # False positives on base classes +reportPropertyTypeMismatch = "error" +reportUninitializedInstanceVariable = "error" +reportUnnecessaryTypeIgnoreComment = "error" +# Exclude from scanning when running pyright +exclude = [ + ".venv/", + # Auto generated, fails some strict pyright checks + "build/", + "src/gen/", +] +# Ignore must be specified for Pylance to stop displaying errors +ignore = [ + # We expect stub files to be incomplete or contain useless statements + "**/*.pyi", +] +reportUnusedCallResult = "none" +# Type stubs may not be completable +reportMissingTypeStubs = "warning" +# False positives with TYPE_CHECKING +reportImportCycles = "information" +# False positives with PySide .connect +reportFunctionMemberAccess = "none" +# Extra runtime safety +reportUnnecessaryComparison = "warning" +# Using Flake8/Ruff instead +reportUnusedImport = "none" +# numpy has way too many complex types that triggers this +reportUnknownMemberType = "none" diff --git a/res/about.ui b/res/about.ui index b0e88fd9..861300cb 100644 --- a/res/about.ui +++ b/res/about.ui @@ -1,158 +1,158 @@ - - - Toufool - AboutAutoSplitWidget - - - - 0 - 0 - 264 - 250 - - - - - 264 - 250 - - - - - 264 - 250 - - - - - 9 - - - - About AutoSplit - - - - :/resources/icon.ico:/resources/icon.ico - - - - - 180 - 220 - 75 - 24 - - - - OK - - - - - - 10 - 44 - 241 - 32 - - - - <html><head/><body><p>Created by <a href="https://twitter.com/toufool"><span style=" text-decoration: underline; color:#0000ff;">Toufool</span></a> and <a href="https://twitter.com/faschz"><span style=" text-decoration: underline; color:#0000ff;">Faschz</span></a><br/>Maintained by <a href="https://twitter.com/Avasam06"><span style=" text-decoration: underline; color:#0000ff;">Avasam</span></a></p></body></html> - - - - - - 10 - 21 - 241 - 16 - - - - Version: - - - - - - 10 - 90 - 241 - 51 - - - - If you enjoy using this program, -please consider donating. -Thank you! - - - Qt::AlignCenter - - - - - - 60 - 150 - 147 - 51 - - - - <html><head/><body><p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&amp;business=BYRHQG69YRHBA&amp;item_name=AutoSplit+development&amp;currency_code=USD&amp;source=url"><img src=":/resources/btn_donateCC_LG.png"/></a></p></body></html> - - - Qt::AlignCenter - - - - - - 190 - 17 - 64 - 64 - - - - - - - :/resources/icon.ico - - - true - - - icon_label - donate_text_label - version_label - created_by_label - ok_button - donate_button_label - - - - - - - ok_button - clicked() - AboutAutoSplitWidget - close() - - - 225 - 210 - - - 153 - 114 - - - - - + + + Toufool + AboutAutoSplitWidget + + + + 0 + 0 + 264 + 250 + + + + + 264 + 250 + + + + + 264 + 250 + + + + + 9 + + + + About AutoSplit + + + + :/resources/icon.ico:/resources/icon.ico + + + + + 180 + 220 + 75 + 24 + + + + OK + + + + + + 10 + 44 + 241 + 32 + + + + <html><head/><body><p>Created by <a href="https://twitter.com/toufool"><span style=" text-decoration: underline; color:#0000ff;">Toufool</span></a> and <a href="https://twitter.com/faschz"><span style=" text-decoration: underline; color:#0000ff;">Faschz</span></a><br/>Maintained by <a href="https://twitter.com/Avasam06"><span style=" text-decoration: underline; color:#0000ff;">Avasam</span></a></p></body></html> + + + + + + 10 + 21 + 241 + 16 + + + + Version: + + + + + + 10 + 90 + 241 + 51 + + + + If you enjoy using this program, +please consider donating. +Thank you! + + + Qt::AlignCenter + + + + + + 60 + 150 + 147 + 51 + + + + <html><head/><body><p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&amp;business=BYRHQG69YRHBA&amp;item_name=AutoSplit+development&amp;currency_code=USD&amp;source=url"><img src=":/resources/btn_donateCC_LG.png"/></a></p></body></html> + + + Qt::AlignCenter + + + + + + 190 + 17 + 64 + 64 + + + + + + + :/resources/icon.ico + + + true + + + icon_label + donate_text_label + version_label + created_by_label + ok_button + donate_button_label + + + + + + + ok_button + clicked() + AboutAutoSplitWidget + close() + + + 225 + 210 + + + 153 + 114 + + + + + diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index ab233e24..1f7aadf4 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -1,26 +1,26 @@ -# Usage: ./scripts/install.ps1 -# -# If you're having issues with the libraries, you might want to first run: -# pip uninstall -y -r ./scripts/requirements-dev.txt -# -# Dependencies --r requirements.txt -# -# Linters & Formatters -add-trailing-comma>=3.1.0 # Must match .pre-commit-config.yaml -autopep8>=2.0.4 # Must match .pre-commit-config.yaml -ruff>=0.1.1 # New checks # Must match .pre-commit-config.yaml -# -# Run `./scripts/designer.ps1` to quickly open the bundled Qt Designer. -# Can also be downloaded externally as a non-python package -# qt6-applications -# Types -types-D3DShot ; sys_platform == 'win32' -types-keyboard -types-Pillow -types-psutil -types-PyAutoGUI -types-pyinstaller -types-pywin32 ; sys_platform == 'win32' -types-requests -types-toml +# Usage: ./scripts/install.ps1 +# +# If you're having issues with the libraries, you might want to first run: +# pip uninstall -y -r ./scripts/requirements-dev.txt +# +# Dependencies +-r requirements.txt +# +# Linters & Formatters +add-trailing-comma>=3.1.0 # Must match .pre-commit-config.yaml +autopep8>=2.0.4 # Must match .pre-commit-config.yaml +ruff>=0.1.1 # New checks # Must match .pre-commit-config.yaml +# +# Run `./scripts/designer.ps1` to quickly open the bundled Qt Designer. +# Can also be downloaded externally as a non-python package +# qt6-applications +# Types +types-D3DShot ; sys_platform == 'win32' +types-keyboard +types-Pillow +types-psutil +types-PyAutoGUI +types-pyinstaller +types-pywin32 ; sys_platform == 'win32' +types-requests +types-toml diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 564d7e67..11dea0e7 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,31 +1,31 @@ -# Requirements file for AutoSplit -# -# Read /docs/build%20instructions.md for more information on how to install, run and build the python code. -# -# Dependencies: -certifi -ImageHash>=4.3.1 # Contains type information + setup as package not module -git+https://github.com/boppreh/keyboard.git#egg=keyboard # Fix install on macos and linux-ci https://github.com/boppreh/keyboard/pull/568 -numpy>=1.23.2 # Python 3.11 wheels -opencv-python-headless>=4.8.1.78 # Typing fixes -packaging -Pillow>=9.2 # gnome-screeshot checks -psutil -PyAutoGUI -PyWinCtl>=0.0.42 # py.typed -PySide6-Essentials>=6.5.1 # fixes incomplete tuple return types https://bugreports.qt.io/browse/PYSIDE-2285 -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 -pyinstaller-hooks-contrib>=2022.9 # opencv-python 4.6 support. Changes for pywintypes and comtypes -# -# https://peps.python.org/pep-0508/#environment-markers -# -# Windows-only dependencies: -pygrabber>=0.2 ; sys_platform == 'win32' # Completed types -pywin32>=301 ; sys_platform == 'win32' -winsdk>=v1.0.0b7 ; sys_platform == 'win32' # Python 3.11 support -git+https://github.com/ranchen421/D3DShot.git#egg=D3DShot ; sys_platform == 'win32' # D3DShot from PyPI with Pillow>=7.2.0 will install 0.1.3 instead of 0.1.5 +# Requirements file for AutoSplit +# +# Read /docs/build%20instructions.md for more information on how to install, run and build the python code. +# +# Dependencies: +certifi +ImageHash>=4.3.1 # Contains type information + setup as package not module +git+https://github.com/boppreh/keyboard.git#egg=keyboard # Fix install on macos and linux-ci https://github.com/boppreh/keyboard/pull/568 +numpy>=1.23.2 # Python 3.11 wheels +opencv-python-headless>=4.8.1.78 # Typing fixes +packaging +Pillow>=9.2 # gnome-screeshot checks +psutil +PyAutoGUI +PyWinCtl>=0.0.42 # py.typed +PySide6-Essentials>=6.5.1 # fixes incomplete tuple return types https://bugreports.qt.io/browse/PYSIDE-2285 +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 +pyinstaller-hooks-contrib>=2022.9 # opencv-python 4.6 support. Changes for pywintypes and comtypes +# +# https://peps.python.org/pep-0508/#environment-markers +# +# Windows-only dependencies: +pygrabber>=0.2 ; sys_platform == 'win32' # Completed types +pywin32>=301 ; sys_platform == 'win32' +winsdk>=v1.0.0b7 ; sys_platform == 'win32' # Python 3.11 support +git+https://github.com/ranchen421/D3DShot.git#egg=D3DShot ; sys_platform == 'win32' # D3DShot from PyPI with Pillow>=7.2.0 will install 0.1.3 instead of 0.1.5 diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index e68093db..1f62fd75 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -1,208 +1,208 @@ -from __future__ import annotations - -import asyncio -from collections import OrderedDict -from dataclasses import dataclass -from enum import Enum, EnumMeta, auto, unique -from itertools import starmap -from typing import TYPE_CHECKING, NoReturn, TypedDict, cast - -from _ctypes import COMError -from pygrabber.dshow_graph import FilterGraph -from typing_extensions import Never, override - -from capture_method.BitBltCaptureMethod import BitBltCaptureMethod -from capture_method.CaptureMethodBase import CaptureMethodBase -from capture_method.DesktopDuplicationCaptureMethod import DesktopDuplicationCaptureMethod -from capture_method.ForceFullContentRenderingCaptureMethod import ForceFullContentRenderingCaptureMethod -from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod -from capture_method.WindowsGraphicsCaptureMethod import WindowsGraphicsCaptureMethod -from utils import WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device - -if TYPE_CHECKING: - from AutoSplit import AutoSplit - - -class Region(TypedDict): - x: int - y: int - width: int - height: int - - -class CaptureMethodMeta(EnumMeta): - # Allow checking if simple string is enum - @override - def __contains__(self, other: object): - try: - self(other) - except ValueError: - return False - return True - - -@unique -# TODO: Try StrEnum in Python 3.11 -class CaptureMethodEnum(Enum, metaclass=CaptureMethodMeta): - # Allow TOML to save as a simple string - @override - def __repr__(self): - return self.value - - @override - def __eq__(self, other: object): - if isinstance(other, str): - return self.value == other - if isinstance(other, Enum): - return self.value == other.value - return other == self - - # Restore hashing functionality for use in Maps - @override - def __hash__(self): - return self.value.__hash__() - - # https://github.com/python/typeshed/issues/10428 - @override - def _generate_next_value_( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] - name: str | CaptureMethodEnum, # noqa: N805 - *_, - ): - return name - - NONE = "" - BITBLT = auto() - WINDOWS_GRAPHICS_CAPTURE = auto() - PRINTWINDOW_RENDERFULLCONTENT = auto() - DESKTOP_DUPLICATION = auto() - VIDEO_CAPTURE_DEVICE = auto() - - -class CaptureMethodDict(OrderedDict[CaptureMethodEnum, type[CaptureMethodBase]]): - def get_index(self, capture_method: str | CaptureMethodEnum): - """Returns 0 if the capture_method is invalid or unsupported.""" - try: - return list(self.keys()).index(cast(CaptureMethodEnum, capture_method)) - except ValueError: - return 0 - - def get_method_by_index(self, index: int): - """ - Returns the `CaptureMethodEnum` at index. - If index is invalid, returns the first (default) `CaptureMethodEnum`. - Returns `CaptureMethodEnum.NONE` if there are no capture methods available. - """ - if len(self) <= 0: - return CaptureMethodEnum.NONE - if index <= 0: - return first(self) - return list(self.keys())[index] - - # Disallow unsafe get w/o breaking it at runtime - @override - def __getitem__( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] - self, - __key: Never, - ) -> NoReturn | type[CaptureMethodBase]: - return super().__getitem__(__key) - - @override - def get(self, key: CaptureMethodEnum, __default: object = None): - """ - Returns the `CaptureMethodBase` subclass for `CaptureMethodEnum` if `CaptureMethodEnum` is available, - else defaults to the first available `CaptureMethodEnum`. - Returns `CaptureMethodBase` directly if there's no capture methods. - """ - if key == CaptureMethodEnum.NONE or len(self) <= 0: - return CaptureMethodBase - return super().get(key, first(self.values())) - - -CAPTURE_METHODS = CaptureMethodDict() -if ( # Windows Graphics Capture requires a minimum Windows Build - WINDOWS_BUILD_NUMBER >= WGC_MIN_BUILD - # Our current implementation of Windows Graphics Capture does not ensure we can get an ID3DDevice - and try_get_direct3d_device() -): - CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = WindowsGraphicsCaptureMethod -CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = BitBltCaptureMethod -try: # Test for laptop cross-GPU Desktop Duplication issue - import d3dshot - - d3dshot.create(capture_output="numpy") -except (ModuleNotFoundError, COMError): - pass -else: - CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = DesktopDuplicationCaptureMethod -CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = ForceFullContentRenderingCaptureMethod -CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = VideoCaptureDeviceCaptureMethod - - -def change_capture_method(selected_capture_method: CaptureMethodEnum, autosplit: AutoSplit): - autosplit.capture_method.close(autosplit) - autosplit.capture_method = CAPTURE_METHODS.get(selected_capture_method)(autosplit) - if selected_capture_method == CaptureMethodEnum.VIDEO_CAPTURE_DEVICE: - autosplit.select_region_button.setDisabled(True) - autosplit.select_window_button.setDisabled(True) - else: - autosplit.select_region_button.setDisabled(False) - autosplit.select_window_button.setDisabled(False) - - -@dataclass -class CameraInfo: - device_id: int - name: str - occupied: bool - backend: str - resolution: tuple[int, int] - - -def get_input_device_resolution(index: int): - filter_graph = FilterGraph() - try: - filter_graph.add_video_input_device(index) - # This can happen with virtual cameras throwing errors. - # For example since OBS 29.1 updated FFMPEG breaking VirtualCam 3.0 - # https://github.com/Toufool/AutoSplit/issues/238 - except COMError: - return None - resolution = filter_graph.get_input_device().get_current_format() - filter_graph.remove_filters() - return resolution - - -async def get_all_video_capture_devices() -> list[CameraInfo]: - named_video_inputs = FilterGraph().get_input_devices() - - async def get_camera_info(index: int, device_name: str): - backend = "" - # Probing freezes some devices (like GV-USB2 and AverMedia) if already in use - # #169 - # FIXME: Maybe offer the option to the user to obtain more info about their devies? - # Off by default. With a tooltip to explain the risk. - # video_capture = cv2.VideoCapture(index) - # video_capture.setExceptionMode(True) - # try: - # # https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html#ga023786be1ee68a9105bf2e48c700294d - # backend = video_capture.getBackendName() # STS_ASSERT - # video_capture.grab() # STS_ERROR - # except cv2.error as error: - # return CameraInfo(index, device_name, True, backend) \ - # if error.code in (cv2.Error.STS_ERROR, cv2.Error.STS_ASSERT) \ - # else None - # finally: - # video_capture.release() - - resolution = get_input_device_resolution(index) - return CameraInfo(index, device_name, False, backend, resolution) \ - if resolution is not None \ - else None - - return [ - camera_info - for camera_info - # Note: Return type required https://github.com/python/typeshed/issues/2652 - in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) - if camera_info is not None - ] +from __future__ import annotations + +import asyncio +from collections import OrderedDict +from dataclasses import dataclass +from enum import Enum, EnumMeta, auto, unique +from itertools import starmap +from typing import TYPE_CHECKING, NoReturn, TypedDict, cast + +from _ctypes import COMError +from pygrabber.dshow_graph import FilterGraph +from typing_extensions import Never, override + +from capture_method.BitBltCaptureMethod import BitBltCaptureMethod +from capture_method.CaptureMethodBase import CaptureMethodBase +from capture_method.DesktopDuplicationCaptureMethod import DesktopDuplicationCaptureMethod +from capture_method.ForceFullContentRenderingCaptureMethod import ForceFullContentRenderingCaptureMethod +from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod +from capture_method.WindowsGraphicsCaptureMethod import WindowsGraphicsCaptureMethod +from utils import WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device + +if TYPE_CHECKING: + from AutoSplit import AutoSplit + + +class Region(TypedDict): + x: int + y: int + width: int + height: int + + +class CaptureMethodMeta(EnumMeta): + # Allow checking if simple string is enum + @override + def __contains__(self, other: object): + try: + self(other) + except ValueError: + return False + return True + + +@unique +# TODO: Try StrEnum in Python 3.11 +class CaptureMethodEnum(Enum, metaclass=CaptureMethodMeta): + # Allow TOML to save as a simple string + @override + def __repr__(self): + return self.value + + @override + def __eq__(self, other: object): + if isinstance(other, str): + return self.value == other + if isinstance(other, Enum): + return self.value == other.value + return other == self + + # Restore hashing functionality for use in Maps + @override + def __hash__(self): + return self.value.__hash__() + + # https://github.com/python/typeshed/issues/10428 + @override + def _generate_next_value_( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + name: str | CaptureMethodEnum, # noqa: N805 + *_, + ): + return name + + NONE = "" + BITBLT = auto() + WINDOWS_GRAPHICS_CAPTURE = auto() + PRINTWINDOW_RENDERFULLCONTENT = auto() + DESKTOP_DUPLICATION = auto() + VIDEO_CAPTURE_DEVICE = auto() + + +class CaptureMethodDict(OrderedDict[CaptureMethodEnum, type[CaptureMethodBase]]): + def get_index(self, capture_method: str | CaptureMethodEnum): + """Returns 0 if the capture_method is invalid or unsupported.""" + try: + return list(self.keys()).index(cast(CaptureMethodEnum, capture_method)) + except ValueError: + return 0 + + def get_method_by_index(self, index: int): + """ + Returns the `CaptureMethodEnum` at index. + If index is invalid, returns the first (default) `CaptureMethodEnum`. + Returns `CaptureMethodEnum.NONE` if there are no capture methods available. + """ + if len(self) <= 0: + return CaptureMethodEnum.NONE + if index <= 0: + return first(self) + return list(self.keys())[index] + + # Disallow unsafe get w/o breaking it at runtime + @override + def __getitem__( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + self, + __key: Never, + ) -> NoReturn | type[CaptureMethodBase]: + return super().__getitem__(__key) + + @override + def get(self, key: CaptureMethodEnum, __default: object = None): + """ + Returns the `CaptureMethodBase` subclass for `CaptureMethodEnum` if `CaptureMethodEnum` is available, + else defaults to the first available `CaptureMethodEnum`. + Returns `CaptureMethodBase` directly if there's no capture methods. + """ + if key == CaptureMethodEnum.NONE or len(self) <= 0: + return CaptureMethodBase + return super().get(key, first(self.values())) + + +CAPTURE_METHODS = CaptureMethodDict() +if ( # Windows Graphics Capture requires a minimum Windows Build + WINDOWS_BUILD_NUMBER >= WGC_MIN_BUILD + # Our current implementation of Windows Graphics Capture does not ensure we can get an ID3DDevice + and try_get_direct3d_device() +): + CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = WindowsGraphicsCaptureMethod +CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = BitBltCaptureMethod +try: # Test for laptop cross-GPU Desktop Duplication issue + import d3dshot + + d3dshot.create(capture_output="numpy") +except (ModuleNotFoundError, COMError): + pass +else: + CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = DesktopDuplicationCaptureMethod +CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = ForceFullContentRenderingCaptureMethod +CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = VideoCaptureDeviceCaptureMethod + + +def change_capture_method(selected_capture_method: CaptureMethodEnum, autosplit: AutoSplit): + autosplit.capture_method.close(autosplit) + autosplit.capture_method = CAPTURE_METHODS.get(selected_capture_method)(autosplit) + if selected_capture_method == CaptureMethodEnum.VIDEO_CAPTURE_DEVICE: + autosplit.select_region_button.setDisabled(True) + autosplit.select_window_button.setDisabled(True) + else: + autosplit.select_region_button.setDisabled(False) + autosplit.select_window_button.setDisabled(False) + + +@dataclass +class CameraInfo: + device_id: int + name: str + occupied: bool + backend: str + resolution: tuple[int, int] + + +def get_input_device_resolution(index: int): + filter_graph = FilterGraph() + try: + filter_graph.add_video_input_device(index) + # This can happen with virtual cameras throwing errors. + # For example since OBS 29.1 updated FFMPEG breaking VirtualCam 3.0 + # https://github.com/Toufool/AutoSplit/issues/238 + except COMError: + return None + resolution = filter_graph.get_input_device().get_current_format() + filter_graph.remove_filters() + return resolution + + +async def get_all_video_capture_devices() -> list[CameraInfo]: + named_video_inputs = FilterGraph().get_input_devices() + + async def get_camera_info(index: int, device_name: str): + backend = "" + # Probing freezes some devices (like GV-USB2 and AverMedia) if already in use + # #169 + # FIXME: Maybe offer the option to the user to obtain more info about their devies? + # Off by default. With a tooltip to explain the risk. + # video_capture = cv2.VideoCapture(index) + # video_capture.setExceptionMode(True) + # try: + # # https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html#ga023786be1ee68a9105bf2e48c700294d + # backend = video_capture.getBackendName() # STS_ASSERT + # video_capture.grab() # STS_ERROR + # except cv2.error as error: + # return CameraInfo(index, device_name, True, backend) \ + # if error.code in (cv2.Error.STS_ERROR, cv2.Error.STS_ASSERT) \ + # else None + # finally: + # video_capture.release() + + resolution = get_input_device_resolution(index) + return CameraInfo(index, device_name, False, backend, resolution) \ + if resolution is not None \ + else None + + return [ + camera_info + for camera_info + # Note: Return type required https://github.com/python/typeshed/issues/2652 + in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) + if camera_info is not None + ]