diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0e57bda38..8a75a5dee 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -32,6 +32,9 @@ on: - "retired.json" - "measurements/**" - "website/**" + # The type check's configuration and baseline. + - "pyproject.toml" + - "mypy-baseline.txt" # Spelled out twice because the workflow parser reads no YAML anchor. A test # holds the two lists equal. push: @@ -58,6 +61,8 @@ on: - "retired.json" - "measurements/**" - "website/**" + - "pyproject.toml" + - "mypy-baseline.txt" permissions: contents: read @@ -163,6 +168,17 @@ jobs: - name: Validate platform configs and emulator profiles run: python scripts/validate_schemas.py --source-only + # The annotations are checked against mypy-baseline.txt, the errors the + # code carried when the check started: a new error fails, and so does + # an entry that no longer occurs until the baseline is synced. mypy + # reads no installed package (no_site_packages in pyproject.toml), so + # what this job has installed does not change the result. + - name: Type-check the scripts and the installer + run: | + pip install "mypy==1.18.2" "mypy-baseline==0.7.4" + mypy > "$RUNNER_TEMP/mypy.txt" || true + mypy-baseline filter --no-colors < "$RUNNER_TEMP/mypy.txt" + # A profile written from a clone can cite lines that do not hold the value # it declares, and schemas and tests pass it. Each profile a change adds is # confronted with its upstream, both roads into main. An edited profile is diff --git a/mypy-baseline.txt b/mypy-baseline.txt new file mode 100644 index 000000000..6aed090c2 --- /dev/null +++ b/mypy-baseline.txt @@ -0,0 +1,974 @@ +install.py:0: error: "Invocation" has no attribute "__iter__" (not iterable) [attr-defined] +install.py:0: error: "None" object is not iterable [misc] +install.py:0: error: "OSError" has no attribute "attempts" [attr-defined] +install.py:0: error: "_Unclosed" has no attribute "name" [attr-defined] +install.py:0: error: "bool" has no attribute "name" [attr-defined] +install.py:0: error: "bool" has no attribute "parent" [attr-defined] +install.py:0: error: "object" has no attribute "get" [attr-defined] +install.py:0: error: "object" has no attribute "get" [attr-defined] +install.py:0: error: "object" has no attribute "rstrip" [attr-defined] +install.py:0: error: "object" object is not iterable [misc] +install.py:0: error: "object" object is not iterable [misc] +install.py:0: error: "str" has no attribute "append" [attr-defined] +install.py:0: error: "str" has no attribute "append" [attr-defined] +install.py:0: error: Argument 1 to "Path" has incompatible type "object"; expected "Union[str, PathLike[str]]" [arg-type] +install.py:0: error: Argument 1 to "Path" has incompatible type "object"; expected "Union[str, PathLike[str]]" [arg-type] +install.py:0: error: Argument 1 to "_ZippedLookup" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 1 to "__iadd__" of "list" has incompatible type "Invocation"; expected "Iterable[str]" [arg-type] +install.py:0: error: Argument 1 to "_digest_file" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_folder_writable" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_listed_names" has incompatible type "bool"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_nearest_folder" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "_replace_released" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "append" of "list" has incompatible type "Optional[CardApp]"; expected "CardApp" [arg-type] +install.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[TuiKey, TuiMouse]"; expected "TuiKey" [arg-type] +install.py:0: error: Argument 1 to "cannot_write_words" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 1 to "chmod" has incompatible type "Optional[Path]"; expected "Union[int, Union[str, bytes, PathLike[str], PathLike[bytes]]]" [arg-type] +install.py:0: error: Argument 1 to "copy_source_refusal" has incompatible type "object"; expected "Optional[Path]" [arg-type] +install.py:0: error: Argument 1 to "first_holding" of "FolderIndex" has incompatible type "set[Optional[Any]]"; expected "set[int]" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[Any]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[Any]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[Any]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "len" has incompatible type "Optional[list[dict[Any, Any]]]"; expected "Sized" [arg-type] +install.py:0: error: Argument 1 to "remove" of "list" has incompatible type "int"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 1 to "replace" has incompatible type "Optional[Path]"; expected "Union[str, bytes, PathLike[str], PathLike[bytes]]" [arg-type] +install.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "Optional[Any]"; expected "str" [arg-type] +install.py:0: error: Argument 1 to "settimeout" of "socket" has incompatible type "object"; expected "Optional[float]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 1 to "shown_path" has incompatible type "Optional[Path]"; expected "Union[Path, str]" [arg-type] +install.py:0: error: Argument 2 to "_browse_to" has incompatible type "object"; expected "Optional[Path]" [arg-type] +install.py:0: error: Argument 2 to "_browse_to" has incompatible type "object"; expected "Optional[Path]" [arg-type] +install.py:0: error: Argument 2 to "_choose_card" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "_choose_card" has incompatible type "object"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "_found_elsewhere" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 2 to "_setup_label" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "_toggle_region" has incompatible type "object"; expected "str" [arg-type] +install.py:0: error: Argument 2 to "core_dirs" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "detected_os" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 2 to "first_holding" of "FolderIndex" has incompatible type "set[Optional[Any]]"; expected "set[str]" [arg-type] +install.py:0: error: Argument 2 to "from_bytes" of "int" has incompatible type "str"; expected "Literal['little', 'big']" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 2 to "insert" of "list" has incompatible type "str"; expected "TuiLine" [arg-type] +install.py:0: error: Argument 3 to "CardSetup" has incompatible type "Optional[str]"; expected "str" [arg-type] +install.py:0: error: Argument 3 to "_choose" has incompatible type "Optional[Path]"; expected "Path" [arg-type] +install.py:0: error: Argument 3 to "_choose" has incompatible type "object"; expected "Path" [arg-type] +install.py:0: error: Argument 3 to "_misplaced" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 3 to "_wrong_contents" has incompatible type "Optional[FolderIndex]"; expected "FolderIndex" [arg-type] +install.py:0: error: Argument 3 to "create_connection" has incompatible type "object"; expected "Optional[Union[tuple[Any, ...], str, Buffer]]" [arg-type] +install.py:0: error: Argument 3 to "keep_regions" has incompatible type "list[str]"; expected "frozenset[Any]" [arg-type] +install.py:0: error: Argument 4 to "_write_verified" has incompatible type "Callable[[], IO[bytes]]"; expected "Callable[[], ContextManager[BinaryIO]]" [arg-type] +install.py:0: error: Argument 6 to "_compose" has incompatible type "tuple[()]"; expected "list[Any]" [arg-type] +install.py:0: error: Argument 6 to "_compose" has incompatible type "tuple[()]"; expected "list[Any]" [arg-type] +install.py:0: error: Argument 6 to "_compose" has incompatible type "tuple[()]"; expected "list[Any]" [arg-type] +install.py:0: error: Argument 7 to "_install_data_member" has incompatible type "object"; expected "list[tuple[str, str]]" [arg-type] +install.py:0: error: Cannot determine type of "cores" [has-type] +install.py:0: error: Cannot determine type of "mount" [has-type] +install.py:0: error: Cannot determine type of "mount" [has-type] +install.py:0: error: Cannot determine type of "plat" [has-type] +install.py:0: error: Cannot determine type of "plat" [has-type] +install.py:0: error: Cannot determine type of "plat" [has-type] +install.py:0: error: Cannot determine type of "regions" [has-type] +install.py:0: error: Cannot determine type of "system" [has-type] +install.py:0: error: Cannot determine type of "system" [has-type] +install.py:0: error: Cannot determine type of "system" [has-type] +install.py:0: error: Cannot determine type of "systems" [has-type] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Cannot infer type of lambda [misc] +install.py:0: error: Generator has incompatible item type "int"; expected "bool" [misc] +install.py:0: error: Incompatible default for argument "after" (default has type "tuple[()]", argument has type "list[Any]") [assignment] +install.py:0: error: Incompatible default for argument "copied" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "cores" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "footer" (default has type "tuple[()]", argument has type "list[Any]") [assignment] +install.py:0: error: Incompatible default for argument "omitted" (default has type "tuple[()]", argument has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible default for argument "pinned" (default has type "tuple[()]", argument has type "list[Any]") [assignment] +install.py:0: error: Incompatible default for argument "regions" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "systems" (default has type "tuple[()]", argument has type "list[str]") [assignment] +install.py:0: error: Incompatible default for argument "trees" (default has type "tuple[()]", argument has type "list[tuple[dict[Any, Any], dict[str, dict[Any, Any]]]]") [assignment] +install.py:0: error: Incompatible return value type (got "IO[bytes]", expected "ContextManager[BinaryIO]") [return-value] +install.py:0: error: Incompatible return value type (got "Optional[Path]", expected "Path") [return-value] +install.py:0: error: Incompatible return value type (got "Union[Any, object]", expected "str") [return-value] +install.py:0: error: Incompatible return value type (got "list[TuiLine]", expected "list[str]") [return-value] +install.py:0: error: Incompatible return value type (got "list[TuiLine]", expected "list[str]") [return-value] +install.py:0: error: Incompatible return value type (got "list[tuple[Optional[str], str]]", expected "list[tuple[str, str]]") [return-value] +install.py:0: error: Incompatible return value type (got "object", expected "dict[Any, Any]") [return-value] +install.py:0: error: Incompatible types in "yield" (actual type "IO[bytes]", expected type "BinaryIO") [misc] +install.py:0: error: Incompatible types in "yield" (actual type "tuple[str, int, Callable[[Any], IO[bytes]]]", expected type "tuple[str, int, Callable[[], BinaryIO]]") [misc] +install.py:0: error: Incompatible types in "yield" (actual type "tuple[str, int, Callable[[Any], Optional[IO[bytes]]]]", expected type "tuple[str, int, Callable[[], BinaryIO]]") [misc] +install.py:0: error: Incompatible types in assignment (expression has type "FolderIndex", base class "tuple" defined the type as "Callable[[Any, SupportsIndex, SupportsIndex], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "FolderIndex", base class "tuple" defined the type as "Callable[[Any, SupportsIndex, SupportsIndex], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "FolderIndex", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "GamesChoice", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "GamesChoice", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "GamesChoice", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[Progress]", variable has type "Progress") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[frozenset[Any]]", variable has type "Optional[tuple[int, str]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[frozenset[Any]]", variable has type "list[str]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[int]", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Path", variable has type "bool") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Union[Any, bool]", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Union[Any, list[Any]]", variable has type "set[str]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "Union[tuple[list[str], list[str], list[str]], str, GamesChoice, None]", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "_TemporaryFileWrapper[bytes]", variable has type "_Unclosed") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "float", variable has type "Optional[int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Any, SupportsIndex, SupportsIndex], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Any], int]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "list[dict[Any, Any]]", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "Invocation") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "set[int]", variable has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "set[int]", variable has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "set[int]", variable has type "list[dict[Any, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", target has type "int") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[Path]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[tuple[list[str], list[str], list[str]]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "bool") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "tuple[int, str]", variable has type "Optional[frozenset[Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]", variable has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]") [assignment] +install.py:0: error: Incompatible types in assignment (expression has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]", variable has type "tuple[tuple[str, Any], tuple[str, Any], tuple[str, Any], tuple[str, Any]]") [assignment] +install.py:0: error: Item "None" of "Optional[BinaryIO]" has no attribute "truncate" [union-attr] +install.py:0: error: Item "None" of "Optional[Element[str]]" has no attribute "iter" [union-attr] +install.py:0: error: Item "None" of "Optional[PackSource]" has no attribute "name" [union-attr] +install.py:0: error: Item "None" of "Optional[PackSource]" has no attribute "name" [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "append" [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "read_text" [union-attr] +install.py:0: error: Item "None" of "Optional[Path]" has no attribute "write_text" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[Terminal]" has no attribute "stream" [union-attr] +install.py:0: error: Item "None" of "Optional[TuiHits]" has no attribute "frame" [union-attr] +install.py:0: error: Item "None" of "Optional[TuiHits]" has no attribute "shown" [union-attr] +install.py:0: error: Item "Path" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "Path" of "Optional[Path]" has no attribute "__iter__" (not iterable) [union-attr] +install.py:0: error: Item "Path" of "Optional[Path]" has no attribute "append" [union-attr] +install.py:0: error: List comprehension has incompatible type List[object]; expected List[str] [misc] +install.py:0: error: List item 0 has incompatible type "int"; expected "FolderIndex" [list-item] +install.py:0: error: List item 0 has incompatible type "tuple[Optional[str], str]"; expected "tuple[str, str]" [list-item] +install.py:0: error: List item 0 has incompatible type "tuple[Optional[str], str]"; expected "tuple[str, str]" [list-item] +install.py:0: error: List item 3 has incompatible type "tuple[Optional[str], str]"; expected "tuple[str, str]" [list-item] +install.py:0: error: Module has no attribute "HKEY_CURRENT_USER" [attr-defined] +install.py:0: error: Module has no attribute "HKEY_LOCAL_MACHINE" [attr-defined] +install.py:0: error: Module has no attribute "LK_NBLCK" [attr-defined] +install.py:0: error: Module has no attribute "OpenKey" [attr-defined] +install.py:0: error: Module has no attribute "QueryValueEx" [attr-defined] +install.py:0: error: Module has no attribute "locking" [attr-defined] +install.py:0: error: Name "found" already defined on line 0 [no-redef] +install.py:0: error: Name "top" already defined on line 0 [no-redef] +install.py:0: error: Need type annotation for "removed" (hint: "removed: list[] = ...") [var-annotated] +install.py:0: error: Need type annotation for "words" (hint: "words: list[] = ...") [var-annotated] +install.py:0: error: No overload variant of "__getitem__" of "list" matches argument type "object" [call-overload] +install.py:0: error: No overload variant of "min" matches argument types "float", "object" [call-overload] +install.py:0: error: Unsupported left operand type for + ("None") [operator] +install.py:0: error: Unsupported left operand type for + ("None") [operator] +install.py:0: error: Unsupported left operand type for + ("None") [operator] +install.py:0: error: Unsupported left operand type for < ("object") [operator] +install.py:0: error: Unsupported operand types for + ("object" and "int") [operator] +install.py:0: error: Unsupported operand types for + ("object" and "int") [operator] +install.py:0: error: Unsupported operand types for / ("Path" and "None") [operator] +install.py:0: error: Unsupported operand types for / ("Path" and "None") [operator] +install.py:0: error: Unsupported operand types for > ("str" and "int") [operator] +install.py:0: error: Unsupported operand types for in ("object" and "str") [operator] +install.py:0: error: Unsupported right operand type for in ("int") [operator] +install.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[dict[Any, Any]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "Optional[tuple[str, str]]" is not indexable [index] +install.py:0: error: Value of type "object" is not indexable [index] +install.py:0: error: Value of type "object" is not indexable [index] +install.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "object" [type-var] +install.py:0: error: Value of type variable "SupportsRichComparisonT" of "sorted" cannot be "Optional[Any]" [type-var] +scripts/campaign.py:0: error: "object" has no attribute "__enter__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__enter__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__enter__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__exit__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__exit__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "__exit__" [attr-defined] +scripts/campaign.py:0: error: "object" has no attribute "decode" [attr-defined] +scripts/campaign.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "bytes") [assignment] +scripts/campaign.py:0: error: Invalid index type "bytes" for "dict[str, str]"; expected type "str" [index] +scripts/campaign.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "get" [union-attr] +scripts/campaign.py:0: error: No overload variant of "Popen" matches argument types "list[str]", "bool", "dict[str, object]" [call-overload] +scripts/campaign.py:0: error: Unpacked dict entry 0 has incompatible type "object"; expected "SupportsKeysAndGetItem[Any, Any]" [dict-item] +scripts/campaign.py:0: error: Unsupported operand types for / ("Path" and "bytes") [operator] +scripts/campaign.py:0: error: Value of type "object" is not indexable [index] +scripts/campaign.py:0: error: Value of type "object" is not indexable [index] +scripts/campaign_profiles.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +scripts/campaign_profiles.py:0: error: Incompatible types in assignment (expression has type "tuple[str, int]", target has type "tuple[int, int]") [assignment] +scripts/campaign_profiles.py:0: error: Unsupported operand types for + ("str" and "int") [operator] +scripts/campaign_rules.py:0: error: "object" not callable [operator] +scripts/campaign_rules.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/campaign_rules.py:0: error: No overload variant of "set" matches argument type "object" [call-overload] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Argument 2 to "unresolved_target_cores" has incompatible type "dict[str, set[str]]"; expected "dict[str, str]" [arg-type] +scripts/check_freshness.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/check_freshness.py:0: error: Need type annotation for "overrides" (hint: "overrides: dict[, ] = ...") [var-annotated] +scripts/check_platform_folder.py:0: error: Argument 1 to "exit" has incompatible type "Union[str, HTTPError]"; expected "Union[str, int, None]" [arg-type] +scripts/check_platform_folder.py:0: error: Argument 1 to "read" of "ZipFile" has incompatible type "Optional[str]"; expected "Union[str, ZipInfo]" [arg-type] +scripts/check_platform_folder.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "dict[str, str]") [assignment] +scripts/check_release_assets.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/common.py:0: error: Argument "sha1_candidates" to "declared_hash_verdict" has incompatible type "list[str]"; expected "set[Any]" [arg-type] +scripts/common.py:0: error: Argument "sha256_candidates" to "declared_hash_verdict" has incompatible type "list[str]"; expected "set[Any]" [arg-type] +scripts/common.py:0: error: Argument 1 to "append" of "list" has incompatible type "tuple[Any, Any, Any]"; expected "str" [arg-type] +scripts/common.py:0: error: Argument 2 to "name_match_size_ok" has incompatible type "str"; expected "Optional[int]" [arg-type] +scripts/common.py:0: error: Cannot determine type of "m" [has-type] +scripts/common.py:0: error: Cannot determine type of "size" [has-type] +scripts/common.py:0: error: Incompatible return value type (got "str", expected "tuple[Optional[str], str]") [return-value] +scripts/common.py:0: error: Incompatible return value type (got "str", expected "tuple[Optional[str], str]") [return-value] +scripts/common.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, str]]", variable has type "Optional[str]") [assignment] +scripts/common.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, str]]", variable has type "Optional[str]") [assignment] +scripts/common.py:0: error: Name "folded" already defined on line 0 [no-redef] +scripts/common.py:0: error: Unpacking a string is disallowed [misc] +scripts/common.py:0: error: Unpacking a string is disallowed [misc] +scripts/common.py:0: error: Unsupported left operand type for & ("None") [operator] +scripts/core_probe.py:0: error: Incompatible types in assignment (expression has type "list[list[Any]]", target has type "Optional[str]") [assignment] +scripts/core_probe.py:0: error: Incompatible types in assignment (expression has type "list[str]", target has type "Optional[str]") [assignment] +scripts/core_probe.py:0: error: Item "None" of "Optional[Path]" has no attribute "name" [union-attr] +scripts/core_probe.py:0: error: Item "None" of "Optional[Path]" has no attribute "open" [union-attr] +scripts/core_probe.py:0: error: Need type annotation for "info" [var-annotated] +scripts/core_probe.py:0: error: Value of type "Optional[Any]" is not indexable [index] +scripts/cross_reference.py:0: error: Need type annotation for "declared" (hint: "declared: dict[, ] = ...") [var-annotated] +scripts/cross_reference.py:0: error: Need type annotation for "platform_data_dirs" (hint: "platform_data_dirs: dict[, ] = ...") [var-annotated] +scripts/cross_reference.py:0: error: Need type annotation for "report" (hint: "report: dict[, ] = ...") [var-annotated] +scripts/dedup.py:0: error: "Sequence[str]" has no attribute "append" [attr-defined] +scripts/derivations.py:0: error: Argument 1 to "_gf2_times" has incompatible type "tuple[int, ...]"; expected "list[int]" [arg-type] +scripts/derivations.py:0: error: Argument 1 to "_gf2_times" has incompatible type "tuple[int, ...]"; expected "list[int]" [arg-type] +scripts/derivations.py:0: error: Argument 1 to "get" of "dict" has incompatible type "tuple[Optional[int], str]"; expected "tuple[int, str]" [arg-type] +scripts/derivations.py:0: error: Incompatible types in assignment (expression has type "Optional[ZipFile]", variable has type "ZipFile") [assignment] +scripts/dumpcatalog.py:0: error: Need type annotation for "snapshots" (hint: "snapshots: dict[, ] = ...") [var-annotated] +scripts/export_native.py:0: error: "object" has no attribute "carries" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "clears" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "component_url" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "destination" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "fills" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "lands" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "name" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "names_anew" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "native_sources" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "native_system" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "platform" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "platform" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "platform_name" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "required_value" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "required_value" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "required_value" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "size" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "size" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "states" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "written" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "written_list" [attr-defined] +scripts/export_native.py:0: error: "object" has no attribute "written_list" [attr-defined] +scripts/export_native.py:0: error: Argument 1 to "_read_archive" has incompatible type "Optional[Any]"; expected "Callable[[bytes], dict[str, str]]" [arg-type] +scripts/export_native.py:0: error: Argument 1 to "_read_archive" has incompatible type "Optional[Any]"; expected "Callable[[bytes], dict[str, str]]" [arg-type] +scripts/exporter/baseline.py:0: error: Incompatible types in assignment (expression has type "Optional[int]", variable has type "int") [assignment] +scripts/exporter/baseline.py:0: error: No overload variant of "int" matches argument type "object" [call-overload] +scripts/exporter/batocera_exporter.py:0: error: Argument 1 to "append" of "list" has incompatible type "tuple[tuple[Any, ...], str]"; expected "tuple[tuple[Any, ...], dict[Any, Any]]" [arg-type] +scripts/exporter/batocera_exporter.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, Optional[str]]]", target has type "Optional[tuple[str, str]]") [assignment] +scripts/exporter/batocera_exporter.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "str") [assignment] +scripts/exporter/batocera_exporter.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/exporter/batocera_exporter.py:0: error: Item "None" of "Union[Any, dict[Any, Any], None]" has no attribute "get" [union-attr] +scripts/exporter/batocera_exporter.py:0: error: Signature of "writable" incompatible with supertype "scripts.exporter.base_exporter.BaseExporter" [override] +scripts/exporter/misterfpga_exporter.py:0: error: "object" has no attribute "size" [attr-defined] +scripts/exporter/misterfpga_exporter.py:0: error: Argument 1 to "written" of "Exporter" has incompatible type "object"; expected "NativeFile" [arg-type] +scripts/exporter/misterfpga_exporter.py:0: error: Argument 1 to "written" of "Exporter" has incompatible type "object"; expected "NativeFile" [arg-type] +scripts/exporter/misterfpga_exporter.py:0: error: Return type "dict[str, Union[str, bytes]]" of "render" incompatible with return type "dict[str, str]" in supertype "scripts.exporter.base_exporter.BaseExporter" [override] +scripts/exporter/retrodeck_exporter.py:0: error: Argument 1 to "add" of "set" has incompatible type "tuple[str, str]"; expected "tuple[str, str, tuple[str, ...]]" [arg-type] +scripts/exporter/retrodeck_exporter.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "OrderedDict[Any, Any]") [assignment] +scripts/exporter/retrodeck_exporter.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "OrderedDict[Any, Any]") [assignment] +scripts/exporter/retrodeck_exporter.py:0: error: Incompatible types in assignment (expression has type "tuple[str, str, tuple[str, ...]]", variable has type "tuple[str, str]") [assignment] +scripts/exporter/retropie_exporter.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/exporter/retropie_exporter.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/exporter/retropie_exporter.py:0: error: Argument 2 to "_extended" of "Exporter" has incompatible type "Optional[tuple[int, int, list[str]]]"; expected "tuple[int, int, list[str]]" [arg-type] +scripts/exporter/retropie_exporter.py:0: error: Expected iterable as variadic argument [misc] +scripts/exporter/retropie_exporter.py:0: error: Value of type "Optional[tuple[int, int, list[str]]]" is not indexable [index] +scripts/exporter/retropie_exporter.py:0: error: Value of type "Optional[tuple[int, int, list[str]]]" is not indexable [index] +scripts/exporter/retropie_exporter.py:0: error: Value of type "Optional[tuple[int, int, list[str]]]" is not indexable [index] +scripts/exporter/romm_exporter.py:0: error: Signature of "writable" incompatible with supertype "scripts.exporter.base_exporter.BaseExporter" [override] +scripts/game_rules.py:0: error: Item "None" of "Optional[dict[str, dict[Any, Any]]]" has no attribute "get" [union-attr] +scripts/generate_db.py:0: error: Name "matched" already defined on line 0 [no-redef] +scripts/generate_db.py:0: error: Need type annotation for "aliases" (hint: "aliases: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "aliases" (hint: "aliases: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "by_name" (hint: "by_name: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "by_path_suffix" (hint: "by_path_suffix: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Need type annotation for "files" (hint: "files: dict[, ] = ...") [var-annotated] +scripts/generate_db.py:0: error: Unsupported left operand type for | ("None") [operator] +scripts/generate_event_schema.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_event_schema.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_event_schema.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_games_index.py:0: error: Argument 1 to "fallback_set" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/generate_installer_doc.py:0: error: "object" has no attribute "add" [attr-defined] +scripts/generate_installer_doc.py:0: error: Argument 1 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "*tuple[object, ...]"; expected "BaseServer" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "*tuple[object, ...]"; expected "Union[socket, tuple[bytes, socket]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "enter_context" of "_BaseExitStack" has incompatible type "object"; expected "AbstractContextManager[Never, Optional[bool]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "join" of "str" has incompatible type "Union[bool, list[str], str, None]"; expected "Iterable[str]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "sample_writers" has incompatible type "_LazySandbox"; expected "Sandbox" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 1 to "startswith" of "str" has incompatible type "Optional[str]"; expected "Union[str, tuple[str, ...]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 2 has incompatible type "*tuple[object, ...]"; expected "Optional[Union[tuple[Any, ...], str, Buffer]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 2 has incompatible type "*tuple[object, ...]"; expected "Optional[float]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 has incompatible type "**dict[str, object]"; expected "Optional[Union[tuple[Any, ...], str, Buffer]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 has incompatible type "**dict[str, object]"; expected "Optional[float]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "**dict[str, object]"; expected "BaseServer" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "__init__" of "SimpleHTTPRequestHandler" has incompatible type "**dict[str, object]"; expected "Union[socket, tuple[bytes, socket]]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "run" of "Sandbox" has incompatible type "**dict[str, bool]"; expected "Optional[Path]" [arg-type] +scripts/generate_installer_doc.py:0: error: Argument 3 to "run" of "Sandbox" has incompatible type "**dict[str, bool]"; expected "tuple[Any, ...]" [arg-type] +scripts/generate_installer_doc.py:0: error: Incompatible return value type (got "tuple[int, ...]", expected "tuple[int, int]") [return-value] +scripts/generate_installer_doc.py:0: error: Incompatible types in assignment (expression has type "Union[str, bytes, bytearray]", variable has type "str") [assignment] +scripts/generate_installer_doc.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_installer_doc.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_installer_doc.py:0: error: Name "_install._OsRule" is not defined [name-defined] +scripts/generate_launcher_index.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_launcher_index.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_launcher_index.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_pack.py:0: error: Argument 1 to "_narrowings" has incompatible type "Union[str, bool]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 1 to "_release_asset_name" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[bool]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[dict[Any, Any]]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[set[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[str]" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 10 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 4 to "_manifest_core_index" has incompatible type "dict_values[str, dict[Any, Any]]"; expected "list[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 4 to "_narrowings" has incompatible type "Union[str, bool]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 5 to "_narrowings" has incompatible type "Union[str, bool]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[bool]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[dict[str, str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[dict[Any, Any]]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[list[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[set[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "Optional[str]" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 7 to "generate_pack" has incompatible type "**dict[str, Union[dict[Any, Any], Any, set[str], list[str], str, bool, None]]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[list[dict[Any, Any]]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[list[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[set[str]]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "bool" [arg-type] +scripts/generate_pack.py:0: error: Argument 9 to "generate_pack" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/generate_pack.py:0: error: Need type annotation for "missing_files" (hint: "missing_files: list[] = ...") [var-annotated] +scripts/generate_pack.py:0: error: Unsupported target for indexed assignment ("Optional[dict[str, PackRecord]]") [index] +scripts/generate_readme.py:0: error: "object" has no attribute "TUI_SYSTEMS_ROW" [attr-defined] +scripts/generate_readme.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_readme.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_readme.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_site.py:0: error: "object" has no attribute "EVERYTHING_IN_PLACE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_CHOOSE_TITLE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_FOLDER_TITLE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_FOUND_TITLE" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_INSTALL_ROW" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_SYSTEMS_ROW" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "TUI_SYSTEMS_ROW" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "emulator_names" [attr-defined] +scripts/generate_site.py:0: error: "object" has no attribute "values" [attr-defined] +scripts/generate_site.py:0: error: "set[str]" has no attribute "get" [attr-defined] +scripts/generate_site.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] +scripts/generate_site.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "tuple[str, str, str]"; expected "tuple[str, str]" [arg-type] +scripts/generate_site.py:0: error: Argument 2 to "_version_said" has incompatible type "Union[Any, dict[Any, Any], None]"; expected "dict[Any, Any]" [arg-type] +scripts/generate_site.py:0: error: Argument 3 to "_blocks_in_runs" has incompatible type "Callable[[list[str]], float]"; expected "Callable[[list[str]], int]" [arg-type] +scripts/generate_site.py:0: error: Argument 3 to "_render_platform_systems" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/generate_site.py:0: error: Argument 4 to "_emulator_part_page" has incompatible type "list[str]"; expected "list[tuple[str, range]]" [arg-type] +scripts/generate_site.py:0: error: Argument 5 to "_card_index" has incompatible type "list[str]"; expected "list[tuple[str, range]]" [arg-type] +scripts/generate_site.py:0: error: Cannot determine type of "entries" [has-type] +scripts/generate_site.py:0: error: Cannot determine type of "entries" [has-type] +scripts/generate_site.py:0: error: Cannot determine type of "entries" [has-type] +scripts/generate_site.py:0: error: Cannot determine type of "stem" [has-type] +scripts/generate_site.py:0: error: Expected iterable as variadic argument [misc] +scripts/generate_site.py:0: error: Generator has incompatible item type "int"; expected "bool" [misc] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", target has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Union[dict[Any, Any], str, None]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "int") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[str, set[str]]", variable has type "set[str]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[str, str]", variable has type "dict[str, tuple[str, dict[str, list[Any]]]]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[dict[str, Sequence[str]]]", target has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", target has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[tuple[str, Any]]", variable has type "Optional[dict[Any, Any]]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "list[tuple[str, list[int]]]", variable has type "list[str]") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "tuple[Any, Union[Any, str]]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "tuple[str, dict[str, list[Any]]]", variable has type "str") [assignment] +scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "tuple[str, str]", variable has type "tuple[str, str, str]") [assignment] +scripts/generate_site.py:0: error: Invalid index type "str" for "dict[tuple[str, str], dict[Any, Any]]"; expected type "tuple[str, str]" [index] +scripts/generate_site.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Optional[str]" has no attribute "startswith" [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Union[Any, dict[Any, Any], None]" has no attribute "get" [union-attr] +scripts/generate_site.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] +scripts/generate_site.py:0: error: Need type annotation for "arcade" (hint: "arcade: list[] = ...") [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "links" (hint: "links: list[] = ...") [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "machines" (hint: "machines: list[] = ...") [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "record" [var-annotated] +scripts/generate_site.py:0: error: Need type annotation for "sample" [var-annotated] +scripts/generate_site.py:0: error: No overload variant of "get" of "dict" matches argument type "str" [call-overload] +scripts/generate_site.py:0: error: Unpacking a string is disallowed [misc] +scripts/generate_site.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "int" is not indexable [index] +scripts/generate_site.py:0: error: Value of type "object" is not indexable [index] +scripts/hashing.py:0: error: No overload variant of "open" matches argument types "object", "str" [call-overload] +scripts/launchers.py:0: error: Argument 1 to "len" has incompatible type "Optional[list[str]]"; expected "Sized" [arg-type] +scripts/launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[dict[Any, Any]]", variable has type "dict[Any, Any]") [assignment] +scripts/launchers.py:0: error: Item "None" of "Optional[list[str]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/measurement_records.py:0: error: Argument 1 to "add" of "set" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/migrate.py:0: error: Incompatible return value type (got "None", expected "tuple[Any, ...]") [return-value] +scripts/pack_probe.py:0: error: Need type annotation for "out" (hint: "out: list[] = ...") [var-annotated] +scripts/pack_probe.py:0: error: Need type annotation for "units" (hint: "units: list[] = ...") [var-annotated] +scripts/packextras.py:0: error: Argument 4 to "_emulator_ships" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/packextras.py:0: error: Item "None" of "Optional[list[dict[Any, Any]]]" has no attribute "append" [union-attr] +scripts/packextras.py:0: error: Need type annotation for "profile" [var-annotated] +scripts/packresolve.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str") [assignment] +scripts/packresolve.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str") [assignment] +scripts/packverify.py:0: error: Incompatible types in assignment (expression has type "Union[Any, str, None]", variable has type "str") [assignment] +scripts/packverify.py:0: error: Incompatible types in assignment (expression has type "dict[str, str]", variable has type "str") [assignment] +scripts/packverify.py:0: error: Invalid index type "str" for "str"; expected type "Union[SupportsIndex, slice[Any, Any, Any]]" [index] +scripts/packverify.py:0: error: Need type annotation for "errors" (hint: "errors: list[] = ...") [var-annotated] +scripts/packverify.py:0: error: Unsupported target for indexed assignment ("str") [index] +scripts/pipeline.py:0: error: "object" has no attribute "members" [attr-defined] +scripts/pipeline.py:0: error: "object" has no attribute "members" [attr-defined] +scripts/pipeline.py:0: error: "object" has no attribute "sources" [attr-defined] +scripts/pipeline.py:0: error: "object" has no attribute "sources" [attr-defined] +scripts/pipeline.py:0: error: Item "None" of "Optional[IO[str]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/profile_sync.py:0: error: "list[tuple[int, int, str]]" has no attribute "items" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "host" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "name" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "name" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" has no attribute "slug" [attr-defined] +scripts/profile_sync.py:0: error: "object" object is not iterable [misc] +scripts/profile_sync.py:0: error: Argument "entry" to "Citation" has incompatible type "Union[dict[Any, Any], list[Any]]"; expected "Optional[dict[Any, Any]]" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "_anchor_tokens" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "append" of "list" has incompatible type "object"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "entry_hashes" has incompatible type "Optional[dict[Any, Any]]"; expected "dict[Any, Any]" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "Optional[int]"; expected "int" [arg-type] +scripts/profile_sync.py:0: error: Argument 1 to "version_tag_candidates" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 4 to "RepoView" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 4 to "_set_pin" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Argument 4 to "_set_pin" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/profile_sync.py:0: error: Cannot determine type of "actual" [has-type] +scripts/profile_sync.py:0: error: Cannot determine type of "old_lines" [has-type] +scripts/profile_sync.py:0: error: Cannot infer type of lambda [misc] +scripts/profile_sync.py:0: error: Cannot infer type of lambda [misc] +scripts/profile_sync.py:0: error: Incompatible return value type (got "object", expected "tuple[Any, list[str]]") [return-value] +scripts/profile_sync.py:0: error: Incompatible types in assignment (expression has type "dict[int, list[Any]]", variable has type "list[tuple[int, int, str]]") [assignment] +scripts/profile_sync.py:0: error: Invalid index type "Optional[int]" for "list[str]"; expected type "SupportsIndex" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, Optional[int], tuple[Any, ...], str]" for "dict[tuple[str, Optional[int]], tuple[RepoView, str]]"; expected type "tuple[str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, Optional[int], tuple[Any, ...], str]" for "dict[tuple[str, Optional[int]], tuple[RepoView, str]]"; expected type "tuple[str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, str, Optional[int], tuple[str, ...], Union[RepoView, Any, None], str]" for "dict[tuple[str, str, Optional[int]], Optional[list[str]]]"; expected type "tuple[str, str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Invalid index type "tuple[str, str, Optional[int], tuple[str, ...], Union[RepoView, Any, None], str]" for "dict[tuple[str, str, Optional[int]], Optional[list[str]]]"; expected type "tuple[str, str, Optional[int]]" [index] +scripts/profile_sync.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "start" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[VersionReport]" has no attribute "declared" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[VersionReport]" has no attribute "latest_release" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "get" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "get" [union-attr] +scripts/profile_sync.py:0: error: Item "None" of "Optional[str]" has no attribute "startswith" [union-attr] +scripts/profile_sync.py:0: error: List item 0 has incompatible type "Optional[int]"; expected "int" [list-item] +scripts/profile_sync.py:0: error: Unsupported operand types for + ("None" and "int") [operator] +scripts/profile_sync.py:0: error: Unsupported target for indexed assignment ("object") [index] +scripts/profile_sync.py:0: error: Value of type "Optional[tuple[int, int]]" is not indexable [index] +scripts/profile_sync.py:0: error: Value of type "object" is not indexable [index] +scripts/profile_sync.py:0: error: Value of type "object" is not indexable [index] +scripts/provenance_report.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "set[Any]") [assignment] +scripts/provenance_report.py:0: error: List comprehension has incompatible type List[tuple[str, str, Any]]; expected List[tuple[str, str]] [misc] +scripts/refresh_data_dirs.py:0: error: Item "None" of "Optional[IO[bytes]]" has no attribute "__enter__" [union-attr] +scripts/refresh_data_dirs.py:0: error: Item "None" of "Optional[IO[bytes]]" has no attribute "__exit__" [union-attr] +scripts/refresh_stale.py:0: error: Argument "area" to "Job" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/refresh_stale.py:0: error: Argument 1 to "_command_for" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/refresh_stale.py:0: error: Argument 1 to "add" of "set" has incompatible type "tuple[Optional[Any], str]"; expected "tuple[str, str]" [arg-type] +scripts/region.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Optional[int]" [type-var] +scripts/region_check.py:0: error: Dict entry 0 has incompatible type "str": "dict[str, Optional[Path]]"; expected "str": "dict[str, Path]" [dict-item] +scripts/region_check.py:0: error: Dict entry 1 has incompatible type "str": "dict[str, Optional[Path]]"; expected "str": "dict[str, Path]" [dict-item] +scripts/region_check.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, Optional[Path]]"; expected "str": "dict[str, Path]" [dict-item] +scripts/safeparse.py:0: error: Invalid base class "base" [misc] +scripts/safeparse.py:0: error: Variable "base" is not valid as a type [valid-type] +scripts/scraper/_hash_merge.py:0: error: Unsupported operand types for + ("None" and "int") [operator] +scripts/scraper/_hash_merge.py:0: error: Unsupported operand types for - ("None" and "int") [operator] +scripts/scraper/arcade_parent_importer.py:0: error: "IO[bytes]" has no attribute "close_archive" [attr-defined] +scripts/scraper/arcade_parent_importer.py:0: error: Argument 1 to "fbneo_sets" has incompatible type "list[Callable[[Any], Any]]"; expected "list[Callable[[], BufferedIOBase]]" [arg-type] +scripts/scraper/arcade_parent_importer.py:0: error: Argument 1 to "read_sets" has incompatible type "list[Callable[[Any], Any]]"; expected "list[Callable[[], BufferedIOBase]]" [arg-type] +scripts/scraper/arcade_parent_importer.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, str]"; expected "str": "str" [dict-item] +scripts/scraper/arcade_parent_importer.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, str]"; expected "str": "str" [dict-item] +scripts/scraper/arcade_parent_importer.py:0: error: Dict entry 2 has incompatible type "str": "dict[str, str]"; expected "str": "str" [dict-item] +scripts/scraper/arcade_parent_importer.py:0: error: Incompatible return value type (got "IO[bytes]", expected "BufferedIOBase") [return-value] +scripts/scraper/arcade_parent_importer.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/scraper/arcade_parent_importer.py:0: error: List comprehension has incompatible type List[Callable[[], BufferedIOBase]]; expected List[Callable[[Any], Any]] [misc] +scripts/scraper/base_scraper.py:0: error: "object" has no attribute "read" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "left" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "stmt" has no attribute "body" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "stmt" has no attribute "body" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/batocera_launchers.py:0: error: Argument "depends_on_option" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument "names" to "Launcher" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "list[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument "passes_to" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to "ref" of "Source" has incompatible type "list[Union[tuple[int, Optional[int]], tuple[int, int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 1 to has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 2 to "_string_lists" has incompatible type "Optional[expr]"; expected "AST" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 2 to "_string_lists" has incompatible type "Optional[expr]"; expected "AST" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 3 to "_mentions" has incompatible type "set[Optional[str]]"; expected "set[str]" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: Argument 3 to "_raise_message" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/batocera_launchers.py:0: error: Incompatible return value type (got "tuple[Union[str, bytes, int, float, complex, ellipsis, None], Union[str, bytes, int, float, complex, ellipsis, None]]", expected "Optional[tuple[str, Any]]") [return-value] +scripts/scraper/batocera_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/scraper/batocera_launchers.py:0: error: Item "None" of "Optional[expr]" has no attribute "lineno" [union-attr] +scripts/scraper/batocera_launchers.py:0: error: List comprehension has incompatible type List[Optional[str]]; expected List[str] [misc] +scripts/scraper/batocera_launchers.py:0: error: List comprehension has incompatible type List[tuple[int, Optional[int]]]; expected List[tuple[int, int]] [misc] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 1 has incompatible type "tuple[Union[int, Any], Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: List item 1 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/batocera_launchers.py:0: error: Need type annotation for "launchers" (hint: "launchers: list[] = ...") [var-annotated] +scripts/scraper/batocera_launchers.py:0: error: Unsupported left operand type for + ("ellipsis") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("bytes" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("complex" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("float" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for + ("int" and "str") [operator] +scripts/scraper/batocera_launchers.py:0: error: Unsupported operand types for in ("Union[str, bytes, int, float, complex, ellipsis]" and "str") [operator] +scripts/scraper/bizhawk_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/bizhawk_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/configgen_launchers.py:0: error: Incompatible return value type (got "AST", expected "Union[Assign, AnnAssign]") [return-value] +scripts/scraper/coreinfo_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/coreinfo_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/coreinfo_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/coreinfo_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Item "None" of "Optional[str]" has no attribute "split" [union-attr] +scripts/scraper/emudeck_launchers.py:0: error: Name "Launcher" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "LauncherPatternMissing" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "Script" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "describe_keys" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "settings" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "source_ref" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Name "words" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_launchers.py:0: error: Need type annotation for "records" (hint: "records: list[] = ...") [var-annotated] +scripts/scraper/emudeck_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "emulator_of_script" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/emudeck_scraper.py:0: error: Need type annotation for "entries" (hint: "entries: list[] = ...") [var-annotated] +scripts/scraper/libretro_scraper.py:0: error: Unpacked dict entry 0 has incompatible type "Collection[str]"; expected "SupportsKeysAndGetItem[Any, Any]" [dict-item] +scripts/scraper/mame_devices.py:0: error: Incompatible types in "yield" (actual type "IO[bytes]", expected type "BufferedIOBase") [misc] +scripts/scraper/mame_hash_scraper.py:0: error: Item "None" of "Optional[dict[str, Any]]" has no attribute "get" [union-attr] +scripts/scraper/minui_scraper.py:0: error: Cannot infer type of lambda [misc] +scripts/scraper/minui_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/minui_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "_read_limited" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/misterfpga_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/myminui_scraper.py:0: error: Name "minui_scraper" already defined (by an import) [no-redef] +scripts/scraper/myminui_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/nextui_scraper.py:0: error: Name "minui_scraper" already defined (by an import) [no-redef] +scripts/scraper/nextui_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "lineno" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "lineno" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "lineno" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "None" object is not iterable [misc] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "args" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "attr" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "comparators" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "elts" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "id" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "values" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "expr" has no attribute "values" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: "stmt" has no attribute "value" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Argument "emulator" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "emulator" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "fails_when_missing" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "names" to "Launcher" has incompatible type "list[Optional[str]]"; expected "list[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "names" to "Launcher" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "list[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "passes_to" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument "slot" to "Launcher" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "index" of "list" has incompatible type "Optional[stmt]"; expected "stmt" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" has incompatible type "*list[Optional[str]]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[Union[str, bytes, int, float, complex, ellipsis, None]]"; expected "Iterable[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "ref" of "Source" has incompatible type "list[tuple[int, Optional[int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "ref" of "Source" has incompatible type "list[tuple[int, Optional[int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 4 to "record" has incompatible type "list[tuple[int, Optional[int]]]"; expected "list[tuple[int, int]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: Argument 5 to "Launcher" has incompatible type "**dict[str, object]"; expected "str" [arg-type] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible return value type (got "Union[str, bytes, int, float, complex, ellipsis, None]", expected "Optional[str]") [return-value] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible return value type (got "dict[str, tuple[list[Union[str, bytes, int, float, complex, ellipsis, None]], AST]]", expected "dict[str, tuple[list[str], stmt]]") [return-value] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible return value type (got "tuple[list[Optional[str]], If]", expected "tuple[list[str], If]") [return-value] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[stmt]", variable has type "stmt") [assignment] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", target has type "str") [assignment] +scripts/scraper/recalbox_launchers.py:0: error: Incompatible types in assignment (expression has type "tuple[Union[str, bytes, int, float, complex, ellipsis, None], Optional[str]]", variable has type "Optional[tuple[str, str]]") [assignment] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[FunctionDef]" has no attribute "args" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[FunctionDef]" has no attribute "name" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[expr]" has no attribute "keys" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[expr]" has no attribute "values" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[list[str]]" has no attribute "__iter__" (not iterable) [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "body" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "lineno" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "lineno" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "None" of "Optional[stmt]" has no attribute "orelse" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "expr" of "Optional[expr]" has no attribute "keys" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "expr" of "Optional[expr]" has no attribute "values" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "stmt" of "Optional[stmt]" has no attribute "body" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: Item "stmt" of "Optional[stmt]" has no attribute "orelse" [union-attr] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "Union[str, bytes, int, float, complex, ellipsis, None]"; expected "str" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[Any, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 0 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "list[Optional[str]]"; expected "str" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "list[tuple[int, Optional[int]]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 1 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 2 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 2 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: List item 3 has incompatible type "tuple[int, Optional[int]]"; expected "tuple[int, int]" [list-item] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "Match" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "MatchAs" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "MatchOr" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Module has no attribute "MatchValue" [attr-defined] +scripts/scraper/recalbox_launchers.py:0: error: Value of type variable "SupportsRichComparisonT" of "max" cannot be "Optional[int]" [type-var] +scripts/scraper/retrobat_launchers.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[str, list[tuple[int, int]]]]", variable has type "str") [assignment] +scripts/scraper/retrobat_launchers.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/scraper/retrobat_scraper.py:0: error: Argument 1 to "urlopen" has incompatible type "BiosRequirement"; expected "Union[str, Request]" [arg-type] +scripts/scraper/retrobat_scraper.py:0: error: Incompatible types in assignment (expression has type "Request", variable has type "BiosRequirement") [assignment] +scripts/scraper/retrobat_scraper.py:0: error: Name "BATOCERA_SLUGS" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "LAUNCHER_FILES" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/retrobat_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Incompatible types in assignment (expression has type "tuple[str, ...]", variable has type "tuple[str, str, str, str]") [assignment] +scripts/scraper/retrodeck_launchers.py:0: error: Name "Launcher" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Name "LauncherPatternMissing" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Name "merge_records" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_launchers.py:0: error: Name "source_ref" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/retrodeck_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_launchers.py:0: error: "int" has no attribute "rstrip" [attr-defined] +scripts/scraper/retropie_launchers.py:0: error: "int" has no attribute "strip" [attr-defined] +scripts/scraper/retropie_launchers.py:0: error: Argument 3 to "Event" has incompatible type "list[str]"; expected "list[list[str]]" [arg-type] +scripts/scraper/retropie_launchers.py:0: error: Argument 3 to "Event" has incompatible type "list[str]"; expected "list[list[str]]" [arg-type] +scripts/scraper/retropie_launchers.py:0: error: Argument 3 to "_walk" of "Scripts" has incompatible type "dict[str, str]"; expected "dict[str, list[str]]" [arg-type] +scripts/scraper/retropie_launchers.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] +scripts/scraper/retropie_launchers.py:0: error: List comprehension has incompatible type List[list[str]]; expected List[str] [misc] +scripts/scraper/retropie_launchers.py:0: error: List comprehension has incompatible type List[list[str]]; expected List[str] [misc] +scripts/scraper/retropie_launchers.py:0: error: List comprehension has incompatible type List[tuple[str, ...]]; expected List[tuple[str, str]] [misc] +scripts/scraper/retropie_launchers.py:0: error: No overload variant of "match" of "Pattern" matches argument type "int" [call-overload] +scripts/scraper/retropie_launchers.py:0: error: No overload variant of "search" of "Pattern" matches argument type "int" [call-overload] +scripts/scraper/retropie_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "SCRIPTMODULES" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "_SCRIPT_SECTIONS" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "fetch_branch_head" already defined (possibly by an import) [no-redef] +scripts/scraper/retropie_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Argument "search_dir" to "Launcher" has incompatible type "object"; expected "Optional[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Union[str, Match[str]]"; expected "str" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 1 to has incompatible type "Optional[int]"; expected "int" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 2 to "next" has incompatible type "None"; expected "Match[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[bool]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[dict[str, list[str]]]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Optional[str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Argument 9 to "Launcher" has incompatible type "**dict[str, object]"; expected "Union[bool, str]" [arg-type] +scripts/scraper/rocknix_launchers.py:0: error: Generator has incompatible item type "str"; expected "Match[str]" [misc] +scripts/scraper/rocknix_launchers.py:0: error: Name "Launcher" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "LauncherPatternMissing" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "Link" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "Script" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "Setting" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "describe_keys" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "settings" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_launchers.py:0: error: Name "source_ref" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Argument 1 to "get" of "dict" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/scraper/rocknix_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "RepoTree" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "extract_launchers" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "md5_field" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/rocknix_scraper.py:0: error: Name "scraper_cli" already defined (possibly by an import) [no-redef] +scripts/scraper/romfolders/card.py:0: error: Incompatible types in assignment (expression has type "list[str]", variable has type "str") [assignment] +scripts/scraper/romfolders/card.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[Match[str]]") [assignment] +scripts/scraper/romfolders/card.py:0: error: No overload variant of "get" of "dict" matches argument types "Optional[Match[str]]", "Optional[Match[str]]" [call-overload] +scripts/scraper/romfolders/es.py:0: error: Argument 2 to "setdefault" of "MutableMapping" has incompatible type "tuple[str, Any, str]"; expected "tuple[str, str, int]" [arg-type] +scripts/scraper/romfolders/es.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str") [assignment] +scripts/scraper/romfolders/es.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +scripts/scraper/romfolders/es.py:0: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment] +scripts/scraper/romfolders/es.py:0: error: List item 0 has incompatible type "None"; expected "dict[Any, Any]" [list-item] +scripts/scraper/romfolders/es.py:0: error: Value of type "Optional[str]" is not indexable [index] +scripts/scraper/romm_scraper.py:0: error: Name "BaseScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "BiosRequirement" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "fetch_github_latest_version" already defined (possibly by an import) [no-redef] +scripts/scraper/romm_scraper.py:0: error: Name "requirement_entry" already defined (possibly by an import) [no-redef] +scripts/scraper/romset_dat_importer.py:0: error: "IO[bytes]" has no attribute "close_archive" [attr-defined] +scripts/scraper/romset_dat_importer.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +scripts/scraper/romset_dat_importer.py:0: error: Incompatible return value type (got "IO[bytes]", expected "ZipExtFile") [return-value] +scripts/scraper/shell_launchers.py:0: error: Name "LauncherShapeError" already defined (possibly by an import) [no-redef] +scripts/scraper/shell_launchers.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/shell_launchers.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "BUILDS_OF" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "ES_SYSTEMS_TEMPLATE" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "RUNNERS" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "_not_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "es_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/amberelec_targets_scraper.py:0: error: Name "xml_systems" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/batocera_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "BUILDS_OF" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "ES_SYSTEMS_TEMPLATE" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "PORTS_TEMPLATE" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "RUNNERS" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "SystemEmulators" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "_not_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "es_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "port_engines" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/emuelec_targets_scraper.py:0: error: Name "with_ports" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/lakka_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/lakka_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "PlatformScraper" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "SYSTEMS_TREE_URL" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "board_cores" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/recalbox_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "fetch_bytes" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "github_headers" already defined (possibly by an import) [no-redef] +scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "standalone_emulators" already defined (possibly by an import) [no-redef] +scripts/sect233r1.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[int, int]]", variable has type "tuple[int, int]") [assignment] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] +scripts/sitediagrams.py:0: error: "AST" has no attribute "value" [attr-defined] +scripts/sitediagrams.py:0: error: Argument 1 to "lstrip" of "bytes" has incompatible type "str"; expected "Optional[Buffer]" [arg-type] +scripts/sitediagrams.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] +scripts/sitediagrams.py:0: error: Incompatible types in assignment (expression has type "Union[str, bytes, int, float, complex, ellipsis, Any, None]", variable has type "str") [assignment] +scripts/sitediagrams.py:0: error: Item "None" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "complex" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "ellipsis" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "float" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/sitediagrams.py:0: error: Item "int" of "Union[str, bytes, int, float, complex, ellipsis, None]" has no attribute "lstrip" [union-attr] +scripts/siteguides.py:0: error: Argument 1 to "tuple" has incompatible type "list[tuple[str, ...]]"; expected "Iterable[tuple[str, str, str, str, str]]" [arg-type] +scripts/siteguides.py:0: error: Incompatible types in assignment (expression has type "tuple[str, ...]", target has type "tuple[str, str, str]") [assignment] +scripts/sitemeasurements.py:0: error: Argument 1 to "permalink" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/sitemeasurements.py:0: error: Argument 2 to "permalink" has incompatible type "Optional[Any]"; expected "str" [arg-type] +scripts/sitemeasurements.py:0: error: Value of type "Optional[Match[str]]" is not indexable [index] +scripts/siterender.py:0: error: "None" object is not iterable [misc] +scripts/siterender.py:0: error: Argument 1 to "add" of "set" has incompatible type "Optional[tuple[str, str]]"; expected "tuple[str, str]" [arg-type] +scripts/sitestarlight.py:0: error: "object" has no attribute "items" [attr-defined] +scripts/sitestarlight.py:0: error: Argument "key" to "max" has incompatible type overloaded function; expected "Callable[[str], Union[SupportsDunderLT[Any], SupportsDunderGT[Any]]]" [arg-type] +scripts/sitestarlight.py:0: error: Incompatible types in assignment (expression has type "Optional[dict[str, int]]", variable has type "dict[str, int]") [assignment] +scripts/slot.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Optional[int]" [type-var] +scripts/sourceref.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/upstream.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/upstream.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/upstream.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] +scripts/upstream.py:0: error: Value of type "object" is not indexable [index] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "add" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "setdefault" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "setdefault" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "Collection[Any]" has no attribute "update" [attr-defined] +scripts/validate_pr.py:0: error: "_MutuallyExclusiveGroup" has no attribute "append" [attr-defined] +scripts/validate_pr.py:0: error: "add" of "set" does not return a value (it only ever returns None) [func-returns-value] +scripts/validate_pr.py:0: error: Argument 1 to "_note_sizes" has incompatible type "Collection[Any]"; expected "dict[str, list[Any]]" [arg-type] +scripts/validate_pr.py:0: error: Incompatible types in assignment (expression has type "list[Never]", variable has type "_MutuallyExclusiveGroup") [assignment] +scripts/validate_pr.py:0: error: Incompatible types in assignment (expression has type "set[Any]", variable has type "list[Any]") [assignment] +scripts/validate_pr.py:0: error: List item 0 has incompatible type "None"; expected "int" [list-item] +scripts/validate_pr.py:0: error: Need type annotation for "checks" (hint: "checks: list[] = ...") [var-annotated] +scripts/validate_pr.py:0: error: Need type annotation for "expected" [var-annotated] +scripts/validate_pr.py:0: error: Need type annotation for "known" [var-annotated] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: "object" has no attribute "get" [attr-defined] +scripts/validate_schemas.py:0: error: Argument 1 to "_semantic_database_checks" has incompatible type "object"; expected "dict[Any, Any]" [arg-type] +scripts/validate_schemas.py:0: error: Argument 3 to "_validate_json_files" has incompatible type "Callable[[Path, dict[Any, Any]], list[str]]"; expected "Optional[Callable[[Path, object], list[str]]]" [arg-type] +scripts/validate_schemas.py:0: error: Argument 3 to "_validate_json_files" has incompatible type "Callable[[Path, dict[Any, Any]], list[str]]"; expected "Optional[Callable[[Path, object], list[str]]]" [arg-type] +scripts/validate_schemas.py:0: error: Unpacked dict entry 0 has incompatible type "object"; expected "SupportsKeysAndGetItem[str, str]" [dict-item] +scripts/validation.py:0: error: "dict[str, str]" has no attribute "startswith" [attr-defined] +scripts/validation.py:0: error: Argument 1 to "_merged_reason" has incompatible type "list[Optional[str]]"; expected "list[str]" [arg-type] +scripts/validation.py:0: error: Argument 1 to "set" has incompatible type "Union[dict[Any, Any], list[str], Any, None]"; expected "Iterable[Any]" [arg-type] +scripts/validation.py:0: error: Argument 1 to "setdefault" of "MutableMapping" has incompatible type "tuple[Optional[Any], Optional[Any]]"; expected "tuple[str, str]" [arg-type] +scripts/validation.py:0: error: Argument 2 to "frontend_digest_matches" has incompatible type "Optional[str]"; expected "str" [arg-type] +scripts/validation.py:0: error: Argument 3 to "_entry_rule" has incompatible type "list[str]"; expected "set[str]" [arg-type] +scripts/validation.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "dict[str, str]") [assignment] +scripts/validation.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/validation.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/validation.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "group" [union-attr] +scripts/validation.py:0: error: Item "None" of "Optional[str]" has no attribute "lower" [union-attr] +scripts/verify.py:0: error: Item "None" of "Optional[dict[Any, Any]]" has no attribute "setdefault" [union-attr] +scripts/version_basis.py:0: error: Too many arguments for "describe_form" [call-arg] diff --git a/pyproject.toml b/pyproject.toml index 508eecc34..bf5a325ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ description = "BIOS collection manager for retrogaming platforms" # install.py is the only entry point a user runs before cloning, and both # bootstraps accept 3.8, so the floor stays where the bootstraps put it. That # floor covers install.py alone: the build scripts under scripts/ need 3.9 -# (pipeline.py checks it), and CI runs the test suite on 3.12. +# (common.py, which every build script imports, checks it), and CI runs the +# test suite on 3.12. requires-python = ">=3.8" dependencies = [ "pyyaml", @@ -37,9 +38,34 @@ dev = [ # installs only in its isolated build environment and 3.12 no longer # bundles: without it the guard skipped on every CI run. "setuptools>=61", + # The type check CI runs, pinned: another mypy reports other errors + # than the ones mypy-baseline.txt holds. + "mypy==1.18.2", + "mypy-baseline==0.7.4", ] # The site is built by Astro Starlight from website/package.json and its # lockfile (npm ci --prefix website): no Python package builds it. [project.urls] Repository = "https://github.com/Abdess/retrobios" + +# The annotations are checked, not only written. Third-party packages are left +# out (no_site_packages) so the result does not depend on what the machine +# has installed: yaml, jsonschema and the rest read as Any everywhere. The +# errors the code carried when the check started sit in mypy-baseline.txt. +# CI fails on an error that is not there and on an entry that no longer +# occurs: `mypy > out.txt`, then `mypy-baseline filter < out.txt`, and +# `mypy-baseline sync < out.txt` once the fix is in. +[tool.mypy] +files = ["install.py", "scripts"] +python_version = "3.9" +explicit_package_bases = true +ignore_missing_imports = true +no_site_packages = true +disable_error_code = ["import-untyped"] +cache_dir = "tmp/mypy_cache" + +[tool.mypy-baseline] +baseline_path = "mypy-baseline.txt" +ignore_categories = ["note"] +sort_baseline = true diff --git a/scripts/bundled_only.py b/scripts/bundled_only.py index d80d5bac5..bf4e34471 100644 --- a/scripts/bundled_only.py +++ b/scripts/bundled_only.py @@ -116,6 +116,9 @@ def main() -> int: parser.add_argument("--json", action="store_true", help="print the list as JSON") parser.add_argument("--strict", action="store_true", help="exit 1 while a file is listed") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "emulators_dir", "platforms_dir", "install_dir"]) rows = bundled_only( load_database(args.db), load_emulator_profiles(args.emulators_dir), diff --git a/scripts/campaign.py b/scripts/campaign.py index fd014113a..1021aee9f 100644 --- a/scripts/campaign.py +++ b/scripts/campaign.py @@ -83,14 +83,17 @@ from collections.abc import Callable, Iterator, Sequence from dataclasses import dataclass, field from pathlib import Path -import yaml - sys.path.insert(0, str(Path(__file__).resolve().parent)) +# First: core_probe stops an interpreter older than 3.9 before anything else +# fails on it. +import core_probe + +import yaml + import autorun import campaign_profiles import campaign_rules as rules -import core_probe import measurement_records as mr ROOT = Path(__file__).resolve().parent.parent diff --git a/scripts/common.py b/scripts/common.py index 2bf7ee504..02c559415 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable from collections import Counter +import argparse import filecmp import hashlib import functools @@ -19,6 +20,16 @@ import signal import sys import zipfile from pathlib import Path +from typing import Any, TextIO + +# Every build script imports this module. verify.py, slots.py, validate_pr.py +# and cross_reference.py started under 3.8 and stopped halfway on a +# str.removeprefix AttributeError, which named no version. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) try: @@ -53,6 +64,33 @@ class DatabaseUnreadable(InputMissing): """The database a command was pointed at does not exist or is not JSON.""" +class SourceUnreadable(ValueError): + """A profile or platform file that is not valid YAML. Not an InputMissing: + the callers that skip a platform whose file is absent must not skip one + whose file is damaged. run_cli prints the message on one line.""" + + +def source_yaml(stream: TextIO) -> Any: + """Parse one of our YAML sources, naming the file a syntax error is in. + + Every profile is loaded whatever the command, so one misindented key in + an unrelated profile stopped `verify.py --emulator handy` on a 37-line + traceback, the faulty file named only near its end. + """ + if yaml is None: + return yaml_load(stream) + try: + return yaml_load(stream) + except yaml.YAMLError as exc: + mark = getattr(exc, "problem_mark", None) + where = f"{stream.name}:{mark.line + 1}" if mark else stream.name + problem = getattr(exc, "problem", None) or str(exc).splitlines()[0] + raise SourceUnreadable( + f"{where}: {problem}. Check it with: " + "python scripts/validate_schemas.py --source-only" + ) from None + + def load_database(db_path: str) -> dict: """Load database.json and return parsed dict. @@ -64,6 +102,18 @@ def load_database(db_path: str) -> dict: with open(db_path) as f: return json.load(f) except FileNotFoundError: + # A mistyped --db beside the real database was told to rescan the + # whole collection into a second file under the typo. + import difflib # noqa: PLC0415 + + typed = Path(db_path) + close = difflib.get_close_matches( + typed.name, [p.name for p in typed.parent.glob("*.json")], n=1, cutoff=0.8 + ) + if close: + raise DatabaseUnreadable( + f"{db_path} not found. Did you mean {typed.with_name(close[0])}?" + ) from None raise DatabaseUnreadable( f"{db_path} not found. Build it with: python scripts/generate_db.py " f"--force --bios-dir bios --output {db_path}" @@ -302,7 +352,7 @@ def _inherited_yaml(platform_name: str, platforms_dir: str) -> tuple[dict, list[ raise FileNotFoundError(f"Platform config not found: {config_file}") with open(config_file) as f: - config = yaml_load(f) or {} + config = source_yaml(f) or {} if "inherits" not in config: return config, [platform_name] @@ -348,7 +398,7 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - reg_real = os.path.realpath(registry_path) if reg_real not in _shared_yml_cache: with open(registry_path) as f: - _shared_yml_cache[reg_real] = yaml_load(f) or {} + _shared_yml_cache[reg_real] = source_yaml(f) or {} reg = _shared_yml_cache[reg_real] # The platform's own entry first, then its parents' for what it does # not say: RetroPie's status, scraper and install are its own, its @@ -387,7 +437,7 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - shared_real = os.path.realpath(shared_path) if shared_real not in _shared_yml_cache: with open(shared_path) as f: - _shared_yml_cache[shared_real] = yaml_load(f) or {} + _shared_yml_cache[shared_real] = source_yaml(f) or {} shared = _shared_yml_cache[shared_real] shared_groups = shared.get("shared_groups", {}) for system in config.get("systems", {}).values(): @@ -494,7 +544,7 @@ def load_data_dir_registry(platforms_dir: str = "platforms") -> dict: if not os.path.exists(registry_path): return {} with open(registry_path) as f: - data = yaml_load(f) or {} + data = source_yaml(f) or {} return data.get("data_directories", {}) @@ -504,7 +554,7 @@ def load_platform_registry(platforms_dir: str = "platforms") -> dict: if not os.path.exists(registry_path): return {} with open(registry_path) as f: - return (yaml_load(f) or {}).get("platforms", {}) + return (source_yaml(f) or {}).get("platforms", {}) def list_registered_platforms(platforms_dir: str = "platforms") -> list[str]: @@ -521,7 +571,7 @@ def list_registered_platforms(platforms_dir: str = "platforms") -> list[str]: if not os.path.exists(registry_path): raise InputMissing(f"no platform registry at {registry_path}") with open(registry_path) as f: - registry = yaml_load(f) or {} + registry = source_yaml(f) or {} platforms = [] for name, meta in sorted(registry.get("platforms", {}).items()): config_path = os.path.join(platforms_dir, meta.get("config", f"{name}.yml")) @@ -621,6 +671,31 @@ def suggest_names( return f"{listing_flag} lists all {total}." +def work_from_repo_root( + args: argparse.Namespace, parser: argparse.ArgumentParser, path_flags: list[str] +) -> None: + """Run from the repository root, whichever directory the command started in. + + database.json records every file from the root and the data registry + names its caches the same way, so a run from scripts/ ended on a + FileNotFoundError traceback. A path the caller typed is read from where + they typed it, a default from the root. + """ + root = Path(__file__).resolve().parent.parent + if Path.cwd().resolve() == root: + return + for flag in path_flags: + value = getattr(args, flag, None) + if value and value != parser.get_default(flag): + # A Path stays a Path for the code that joins onto it. + setattr(args, flag, type(value)(os.path.abspath(value))) + os.chdir(root) + if getattr(args, "platform", None): + # Read before the move, the registry was not there to map + # "RetroArch" to its slug. + args.platform = platform_arg(args.platform) + + def platform_arg(value: str) -> str: """--platform as typed, mapped to the registry's slug when it names one. @@ -669,7 +744,7 @@ def _target_overrides(platform_name: str, targets_dir: str) -> dict: if not os.path.exists(overrides_file): return {} with open(overrides_file) as f: - all_overrides = yaml_load(f) or {} + all_overrides = source_yaml(f) or {} return all_overrides.get(platform_name, {}).get("targets", {}) @@ -690,7 +765,7 @@ def platform_files_target(platform: str, target: str, platforms_dir: str = "plat if not os.path.exists(target_file): return False with open(target_file) as f: - targets = (yaml_load(f) or {}).get("targets", {}) + targets = (source_yaml(f) or {}).get("targets", {}) return target in _target_alias_index(targets, _target_overrides(platform, targets_dir)) @@ -710,7 +785,7 @@ def canonical_target_name( if not os.path.exists(target_file): continue with open(target_file) as f: - targets = (yaml_load(f) or {}).get("targets", {}) + targets = (source_yaml(f) or {}).get("targets", {}) canonical = _target_alias_index( targets, _target_overrides(platform_name, targets_dir) ).get(target) @@ -764,7 +839,7 @@ def load_target_config( f"(platforms with targets: {', '.join(with_targets)})" ) with open(target_file) as f: - data = yaml_load(f) or {} + data = source_yaml(f) or {} targets = data.get("targets", {}) overrides = _target_overrides(platform_name, targets_dir) @@ -822,7 +897,7 @@ def list_available_targets( if not os.path.exists(target_file): return [] with open(target_file) as f: - data = yaml_load(f) or {} + data = source_yaml(f) or {} # The count --target filters on: overrides applied, as load_target_config # applies them. The scraped list counted Batocera's machine variants and @@ -2229,7 +2304,7 @@ def load_emulator_profiles( raise InputMissing(f"no emulator profile directory at {emulators_dir}") for f in source_yaml_paths(emu_path): with open(f) as fh: - profile = yaml_load(fh) or {} + profile = source_yaml(fh) or {} if "emulator" not in profile: continue _normalize_hash_fields(profile) @@ -2359,7 +2434,7 @@ def group_identical_platforms( try: raw_path = os.path.join(platforms_dir, f"{platform}.yml") with open(raw_path) as f: - raw = yaml_load(f) or {} + raw = source_yaml(f) or {} inherits[platform] = "inherits" in raw config = load_platform_config(platform, platforms_dir) except FileNotFoundError: @@ -3503,6 +3578,9 @@ def build_target_cores_cache( raise notes.append(f"INFO: Skipping {p}: no hardware targets") skip.append(p) + except SourceUnreadable: + # A damaged targets file is not a platform without the target. + raise except ValueError as e: if is_all: notes.append(f"INFO: Skipping {p}: {e}") @@ -3530,7 +3608,7 @@ def _unknown_target_message(target: str, platforms: list[str], platforms_dir: st if not os.path.exists(target_file): continue with open(target_file) as f: - targets = (yaml_load(f) or {}).get("targets", {}) + targets = (source_yaml(f) or {}).get("targets", {}) overrides = _target_overrides(platform, targets_dir) for name in _target_alias_index(targets, overrides): owners.setdefault(name, []).append(platform) @@ -3624,10 +3702,18 @@ def run_cli(main: Callable[[], int | None]) -> None: for name in ("SIGTERM", "SIGHUP"): if hasattr(signal, name): signal.signal(getattr(signal, name), _exit_on_signal) + # Redirected, stdout is block-buffered and stderr is not: the --strict + # verdict landed on the first line of the log, before the report it + # closes. Line buffering keeps the two in the order they were written. + # A stream a caller swapped in (a test's StringIO) has nothing to set. + reconfigure = getattr(sys.stdout, "reconfigure", None) + if reconfigure is not None: + reconfigure(line_buffering=True) try: code = main() sys.stdout.flush() - except (InputMissing, ArtifactLockBusy, ArtifactDirectoryMissing) as exc: + except (InputMissing, SourceUnreadable, ArtifactLockBusy, + ArtifactDirectoryMissing) as exc: print(f"ERROR: {exc}", file=sys.stderr) sys.exit(1) except BrokenPipeError: diff --git a/scripts/core_probe.py b/scripts/core_probe.py index 4d3cc7f1c..d58142c78 100644 --- a/scripts/core_probe.py +++ b/scripts/core_probe.py @@ -40,6 +40,14 @@ from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass, field from pathlib import Path +# The probe tools do not import common.py, which states the 3.9 floor for the +# other scripts. Under 3.8 they stopped on a str.removesuffix AttributeError. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) + ROOT = Path(__file__).resolve().parent.parent MIN_BENCH_REPETITIONS = 5 DEFAULT_WORK_DIR = ROOT / "tmp" / "core_probe" diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index f92545b27..365efefda 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -1191,6 +1191,9 @@ def main() -> None: "--list-emulators", action="store_true", help="List available emulators" ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["emulators_dir", "platforms_dir", "db"]) if args.list_emulators: ignored = [flag for flag, on in ( ("--emulator", args.emulator), ("--platform", args.platform), diff --git a/scripts/game_rules.py b/scripts/game_rules.py index 040884889..d9512ba34 100644 --- a/scripts/game_rules.py +++ b/scripts/game_rules.py @@ -247,6 +247,9 @@ def main() -> int: help="profiles the rules are checked against (default emulators)") parser.add_argument("--json", action="store_true", help="print the summary as JSON") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["regions", "extras", "headers", "emulators_dir"]) regions = load_yaml(args.regions) extras = load_yaml(args.extras) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 03ef16791..3934830a9 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -43,6 +43,7 @@ from common import ( require_directory, target_listing_line, suggest_names, + system_index, require_platform, load_target_config, archive_entry, @@ -164,7 +165,9 @@ class PackRecord: self.sources: dict[str, str] = {} -def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> None: +def _add_pack_member( + zf: zipfile.ZipFile | PackRecord, source_path: str, arcname: str +) -> None: """Add a file to the pack with metadata that does not depend on the build. ZipFile.write copies the source file's mtime into the member. For a file @@ -197,7 +200,9 @@ def _add_pack_member(zf: zipfile.ZipFile, source_path: str, arcname: str) -> Non shutil.copyfileobj(source, target, 1024 * 1024) -def _write_generated_member(zf: zipfile.ZipFile, arcname: str, text: str) -> None: +def _write_generated_member( + zf: zipfile.ZipFile | PackRecord, arcname: str, text: str +) -> None: """Write a pack member the builder composes rather than copies. READMEs, instruction notes and the manifest are produced at build time, @@ -581,7 +586,7 @@ def _data_directory_members( def _pack_data_directories( - zf: zipfile.ZipFile, + zf: zipfile.ZipFile | PackRecord, pack_systems: dict, data_registry: dict | None, platform_name: str, @@ -1476,7 +1481,9 @@ def generate_pack( def _extract_zip_to_archive( - source_zip: str, dest_prefix: str, target_zf: zipfile.ZipFile + source_zip: str, + dest_prefix: str, + target_zf: zipfile.ZipFile | PackRecord, ) -> None: """Extract contents of a source ZIP into target ZIP under dest_prefix.""" with zipfile.ZipFile(source_zip, "r") as src: @@ -1507,7 +1514,7 @@ def _extract_zip_to_archive( def _add_zip_to_pack( source_zip: str, dest_path: str, - target_zf: zipfile.ZipFile, + target_zf: zipfile.ZipFile | PackRecord, file_entry: dict | None = None, ) -> None: """Write a source ZIP into the pack, byte for byte. @@ -1555,7 +1562,7 @@ class DamagedArchiveError(OSError): def _pack_data_tree( - zf: zipfile.ZipFile, + zf: zipfile.ZipFile | PackRecord, local_cache: str, dd_dest: str, seen_destinations: set[str], @@ -2053,7 +2060,10 @@ def generate_system_pack( f"No emulators found for system(s): {', '.join(system_ids)}", file=sys.stderr, ) - print(suggest_names(system_ids, sorted(all_systems), "--list-systems"), + # --list-systems merges spellings that differ in case or separator: + # the hint counted 579 spellings for the 532 lines it prints. + print(suggest_names(system_ids, sorted(all_systems), "--list-systems", + listed=len(system_index(profiles))), file=sys.stderr) return None diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 22ac647b6..67898e5ec 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -14,7 +14,6 @@ Usage: from __future__ import annotations -from siterender import SECTION_LABELS, WIKI_SRC_DIR import argparse import csv import hashlib @@ -64,6 +63,7 @@ from common import ( unique_emulator_profiles, yaml_load, ) +from siterender import SECTION_LABELS, WIKI_SRC_DIR from artifacts import ArtifactLockBusy, artifact_lock from artifacts import json_text as _json_text from cross_reference import FileConsumers, entry_target, held_copy diff --git a/scripts/identity.py b/scripts/identity.py index 745029f96..188e899d1 100644 --- a/scripts/identity.py +++ b/scripts/identity.py @@ -146,6 +146,9 @@ def main() -> int: "--strict", action="store_true", help="exit non-zero when any is found" ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["bios_dir"]) from common import require_directory # noqa: PLC0415 require_directory(args.bios_dir) diff --git a/scripts/list_platforms.py b/scripts/list_platforms.py index 82f8e4c31..88fa39319 100644 --- a/scripts/list_platforms.py +++ b/scripts/list_platforms.py @@ -26,7 +26,10 @@ def list_platforms() -> list[str]: def main() -> None: parser = argparse.ArgumentParser(allow_abbrev=False, description="List available platforms") - parser.parse_args() + args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, []) platforms = list_platforms() diff --git a/scripts/provenance_report.py b/scripts/provenance_report.py index a54c1b0a6..16ec2f5f8 100644 --- a/scripts/provenance_report.py +++ b/scripts/provenance_report.py @@ -384,6 +384,9 @@ def main() -> int: "(the pipeline writes derivable.json)", ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "provenance_dir", "emulators_dir", "write_derivable"]) if args.write_derivable and not args.derive: parser.error("--write-derivable records what --derive finds: pass both") diff --git a/scripts/region_audit.py b/scripts/region_audit.py index eb02e2e6f..731e0d2a8 100644 --- a/scripts/region_audit.py +++ b/scripts/region_audit.py @@ -201,6 +201,9 @@ def main() -> int: ) parser.add_argument("--json", action="store_true", help="Machine-readable output") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "emulators_dir"]) db = load_database(args.db) profiles = load_emulator_profiles(args.emulators_dir) diff --git a/scripts/region_check.py b/scripts/region_check.py index b7eff324f..6fa9a9019 100644 --- a/scripts/region_check.py +++ b/scripts/region_check.py @@ -36,6 +36,14 @@ import urllib.request from collections.abc import Callable, Iterable from pathlib import Path +# The probe tools do not import common.py, which states the 3.9 floor for the +# other scripts. Under 3.8 they stopped on a str.removesuffix AttributeError. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) + sys.path.insert(0, str(Path(__file__).resolve().parent)) import yaml # noqa: E402 diff --git a/scripts/slots.py b/scripts/slots.py index b261d6c74..8f62ed3e4 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -1009,6 +1009,9 @@ def main() -> int: "without it the run reports them and exits 0", ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "platforms_dir", "emulators_dir"]) import contextlib diff --git a/scripts/verify.py b/scripts/verify.py index 5f909d155..dcd9eb37a 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -41,8 +41,10 @@ from common import ( _foreign_tree, _owner_tokens, platform_arg, + work_from_repo_root, target_listing_line, suggest_names, + system_index, require_platform, archive_entry, archive_is_optional, @@ -129,29 +131,6 @@ DEFAULT_EMULATORS_DIR = "emulators" REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -def work_from_repo_root( - args: argparse.Namespace, parser: argparse.ArgumentParser, path_flags: list[str] -) -> None: - """Run from the repository root, whichever directory the command started in. - - database.json records every file from the root and the data registry - names its caches the same way, so a run from scripts/ ended on a - FileNotFoundError traceback. A path the caller typed is read from where - they typed it, a default from the root. - """ - if os.path.realpath(os.getcwd()) == os.path.realpath(REPO_ROOT): - return - for flag in path_flags: - value = getattr(args, flag, None) - if value and value != parser.get_default(flag): - setattr(args, flag, os.path.abspath(value)) - os.chdir(REPO_ROOT) - if getattr(args, "platform", None): - # Read before the move, the registry was not there to map - # "RetroArch" to its slug. - args.platform = platform_arg(args.platform) - - # Status model: aligned with Batocera BiosStatus (batocera-systems:967-969) @@ -1808,7 +1787,8 @@ def _print_undeclared_entry( ref_part = f" [{u['source_ref']}]" if u.get("source_ref") else "" print(f" validates {checks_label}{ref_part}") else: - print(f" [{'+'.join(checks)}]") + # A bare "[size]" under the entry said nothing to a reader. + print(f" validates {'+'.join(checks)}") def _split_undeclared(undeclared: list[dict]) -> tuple[list[dict], list[dict], list[dict]]: @@ -1920,10 +1900,15 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None: u for u in bundled if u["required"] and u.get("category", "bios") != "game_data" ] - names = ", ".join(sorted({u["name"] for u in shown})) + named = sorted({u["name"] for u in shown}) + # The required firmware alone read as the whole count: 14 names + # for 154 entries, nothing saying the rest existed. + hidden = len({u["name"] for u in bundled}) - len(named) + more = f"{hidden} more, " if named else "" + items = named + ([f"{more}--verbose lists them"] if hidden else []) print( f" Bundled with their emulator, not collected: {len(bundled)}" - + (f" ({names})" if names else "") + + (f" ({', '.join(items)})" if items else "") ) if unplaced: print(f" Core files held but not packed: {len(unplaced)} " @@ -2611,7 +2596,10 @@ def verify_system( f"No emulators found for system(s): {', '.join(system_ids)}", file=sys.stderr, ) - print(suggest_names(system_ids, sorted(all_systems), "--list-systems"), + # --list-systems merges spellings that differ in case or separator: + # the hint counted 579 spellings for the 532 lines it prints. + print(suggest_names(system_ids, sorted(all_systems), "--list-systems", + listed=len(system_index(profiles))), file=sys.stderr) sys.exit(1) @@ -2821,7 +2809,7 @@ def _main(held: contextlib.ExitStack) -> None: parser = argparse.ArgumentParser(allow_abbrev=False, description="Platform-native BIOS verification") parser.add_argument("--platform", "-p", type=platform_arg, help="Platform name") parser.add_argument( - "--all", action="store_true", help="Verify all active platforms" + "--all", action="store_true", help="Verify every registered platform" ) parser.add_argument( "--emulator", "-e", help="Emulator profile name(s), comma-separated" @@ -3121,6 +3109,13 @@ def _overall_summary(group_results: list[tuple[dict, list[str]]]) -> str: named = [f"{name} {count}" for name, count in counts.items() if count] return f" ({', '.join(named)})" if named else "" + def on(groups: tuple[str, ...]) -> str: + """The platforms a file is missing on, by the shorter side.""" + if len(group_results) < 2 or len(groups) * 2 <= len(group_results): + return ", ".join(groups) + others = [" / ".join(g) for _, g in group_results if " / ".join(g) not in groups] + return "every platform" + (f" but {', '.join(others)}" if others else "") + parts = [] for severity, label in ((Severity.CRITICAL, "critical"), (Severity.WARNING, "warning"), (Severity.INFO, "info")): @@ -3139,18 +3134,26 @@ def _overall_summary(group_results: list[tuple[dict, list[str]]]) -> str: parts.append(f"{total} {'discrepancy' if total == 1 else 'discrepancies'}{where(counts)}") lines = [f"{platforms} platforms: {', '.join(parts)}"] - missing = { - " / ".join(group): sorted({ + # Grouped by file, not by platform: sfiii2a.zip missing everywhere was + # counted and printed once per platform, 24 files for 10 distinct ones. + holders: dict[str, list[str]] = {} + for r, group in group_results: + for name in sorted({ u["name"] for u in _core_required_missing(_split_undeclared( r.get("undeclared_files", []))[1]) - }) - for r, group in group_results - } - total = sum(len(names) for names in missing.values()) - if total: - detail = "; ".join(f"{group}: {', '.join(names)}" - for group, names in missing.items() if names) - lines.append(f"{total} core file(s) a core cannot run without are missing ({detail})") + }): + holders.setdefault(name, []).append(" / ".join(group)) + if holders: + by_platforms: dict[tuple[str, ...], list[str]] = {} + for name, groups in sorted(holders.items()): + by_platforms.setdefault(tuple(groups), []).append(name) + detail = "; ".join( + f"{', '.join(names)} ({on(groups)})" + for groups, names in sorted(by_platforms.items(), + key=lambda item: (-len(item[0]), item[1])) + ) + lines.append(f"{len(holders)} core file(s) a core cannot run without are " + f"missing: {detail}") for category, label in _NOT_FIRMWARE.items(): counts = { " / ".join(group): len((r.get("missing_not_firmware") or {}).get(category, [])) diff --git a/tests/test_adler32_resolution.py b/tests/test_adler32_resolution.py index 5c6f2f502..c90aba144 100644 --- a/tests/test_adler32_resolution.py +++ b/tests/test_adler32_resolution.py @@ -9,6 +9,8 @@ corrected it later, every other reader of the resolution did not. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import shutil diff --git a/tests/test_agnostic_scan.py b/tests/test_agnostic_scan.py index 94d6d3680..254527ca3 100644 --- a/tests/test_agnostic_scan.py +++ b/tests/test_agnostic_scan.py @@ -12,6 +12,8 @@ BIOS root of platforms that do not run that emulator. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_agnostic_scan_mode.py b/tests/test_agnostic_scan_mode.py index 7e165aa02..c8db42d81 100644 --- a/tests/test_agnostic_scan_mode.py +++ b/tests/test_agnostic_scan_mode.py @@ -8,6 +8,8 @@ libretro core, while verify set the entry aside. Reads the repository. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_agnostic_scope.py b/tests/test_agnostic_scope.py index fac69e8e7..3a1d3d27a 100644 --- a/tests/test_agnostic_scope.py +++ b/tests/test_agnostic_scope.py @@ -9,6 +9,8 @@ was not where it looks. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_agnostic_substitute.py b/tests/test_agnostic_substitute.py index 8ba581e2f..8b30d53d7 100644 --- a/tests/test_agnostic_substitute.py +++ b/tests/test_agnostic_substitute.py @@ -7,6 +7,8 @@ the slot missing and the manifest omitted it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import tempfile diff --git a/tests/test_amberelec_targets.py b/tests/test_amberelec_targets.py index 3724fd0d5..5abb32518 100644 --- a/tests/test_amberelec_targets.py +++ b/tests/test_amberelec_targets.py @@ -8,6 +8,8 @@ the firmware of cores its image never had. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_arcade_parents.py b/tests/test_arcade_parents.py index c03b4caa1..ca50d79a3 100644 --- a/tests/test_arcade_parents.py +++ b/tests/test_arcade_parents.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import tempfile diff --git a/tests/test_archive_choice.py b/tests/test_archive_choice.py index cee358bae..65cc92ddb 100644 --- a/tests/test_archive_choice.py +++ b/tests/test_archive_choice.py @@ -7,6 +7,8 @@ seven of the eight members Batocera checks were missing. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_archive_recipes.py b/tests/test_archive_recipes.py index cdfbd1f63..551cf5d9d 100644 --- a/tests/test_archive_recipes.py +++ b/tests/test_archive_recipes.py @@ -9,6 +9,8 @@ those members were never read there, and the archive matched no recipe. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import unittest diff --git a/tests/test_artifact_lock.py b/tests/test_artifact_lock.py index 72804b1cb..c99ce7c4d 100644 --- a/tests/test_artifact_lock.py +++ b/tests/test_artifact_lock.py @@ -7,6 +7,8 @@ ZIPs, which surfaces as BadZipFile far from its cause. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import subprocess import sys diff --git a/tests/test_atomic_artifacts.py b/tests/test_atomic_artifacts.py index f506b7573..51c652114 100644 --- a/tests/test_atomic_artifacts.py +++ b/tests/test_atomic_artifacts.py @@ -8,6 +8,8 @@ path in bios/: a scan running beside the copy hashed the truncated file. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index a93c0a8c8..c5e4a8ecc 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import contextlib import hashlib @@ -136,6 +138,41 @@ class ReadmeRegressions(unittest.TestCase): self.assertNotIn("SyntaxError", proc.stderr) self.assertIn("generate_pack.py needs Python 3.9", proc.stderr) + def test_every_script_names_the_floor_under_an_older_interpreter(self): + """Only generate_pack.py and pipeline.py checked the version. verify.py, + slots.py, validate_pr.py and cross_reference.py started under 3.8 and + stopped halfway on a str.removeprefix AttributeError.""" + import shutil # noqa: PLC0415 + + python38 = shutil.which("python3.8") + if python38 is None: + if os.environ.get("CI"): + self.fail("CI must provide python3.8") + self.skipTest("no python3.8 on PATH") + commands = ( + ["verify.py", "--emulator", "handy"], + ["verify.py", "--platform", "batocera"], + ["validate_pr.py", "--json", "README.md"], + ["cross_reference.py", "--emulator", "handy"], + ["slots.py"], + ["identity.py"], + ["generate_site.py", "--help"], + ["campaign.py", "--help"], + ["region_check.py", "--help"], + ) + for command in commands: + with self.subTest(command=" ".join(command)): + # -E -s rather than -I: -I drops the script's folder from + # sys.path, so the imports would fail before any check. + proc = subprocess.run( + [python38, "-E", "-s", f"scripts/{command[0]}", *command[1:]], + cwd=ROOT, capture_output=True, text=True, timeout=60, + check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + self.assertIn(f"{command[0]} needs Python 3.9", proc.stderr) + def test_the_floor_interpreter_imports_every_script(self): """Every module under scripts/ is loaded by a real interpreter of the floor version, as the installer's 3.8 floor is proven by running it. @@ -218,6 +255,46 @@ class AModuleRunAloneHasItsScratchDirectory(unittest.TestCase): self.assertEqual(proc.stdout.split(), [scratch, scratch]) +class EveryTestModuleImportsTheIsolation(unittest.TestCase): + """tests/__init__.py keeps a run offline, its scratch under tmp/ and its + installer cache its own, but only `discover -s tests -t .` imported it: + `discover tests` ran with RETROBIOS_OFFLINE unset, scratch in /tmp and + the user's real cache. Each module now imports the package first.""" + + def test_the_package_import_comes_before_any_other(self): + missing = [] + for path in sorted((ROOT / "tests").glob("test_*.py")): + body = ast.parse(path.read_text(encoding="utf-8")).body + imports = [ + node for node in body + if isinstance(node, (ast.Import, ast.ImportFrom)) + and getattr(node, "module", None) != "__future__" + ] + first = imports[0] if imports else None + if not (isinstance(first, ast.Import) + and [alias.name for alias in first.names] == ["tests"]): + missing.append(path.name) + self.assertEqual(missing, []) + + def test_discover_without_a_top_level_runs_isolated(self): + env = {k: v for k, v in os.environ.items() + if k not in ("RETROBIOS_OFFLINE", "TMPDIR", "RETROBIOS_CACHE")} + proc = subprocess.run( + [sys.executable, "-c", + "import os, sys, tempfile, unittest\n" + "unittest.TestLoader().discover('tests', pattern='test_artifact_lock.py')\n" + "print(os.environ.get('RETROBIOS_OFFLINE'), tempfile.gettempdir()," + " os.environ.get('RETROBIOS_CACHE'))\n"], + cwd=ROOT, env=env, capture_output=True, text=True, timeout=120, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr) + offline, scratch, cache = proc.stdout.split() + self.assertEqual(offline, "1") + self.assertEqual(scratch, str(ROOT / "tmp" / "tests")) + self.assertEqual(cache, str(ROOT / "tmp" / "tests" / "cache")) + + class ReleaseSigningRegressions(unittest.TestCase): """A checksum list published beside its own artifacts proves nothing. @@ -2834,6 +2911,50 @@ class DeclaredDependenciesMatchTheDocumentedWorkflows(unittest.TestCase): ) +class TheAnnotationsAreChecked(unittest.TestCase): + """Every signature carried type hints and nothing read them: six pack + helpers declared `zipfile.ZipFile` while every caller also passed a + PackRecord, and 990 such contradictions went through CI green. mypy now + runs in CI against mypy-baseline.txt, the errors the code carried when the + check started.""" + + BASELINE = ROOT / "mypy-baseline.txt" + ENTRY = re.compile(r"(?P[^:]+\.py):0: error: .+ \[[a-z-]+\]") + + def test_ci_holds_the_code_to_the_baseline(self): + body = (ROOT / ".github" / "workflows" / "validate.yml").read_text( + encoding="utf-8" + ) + self.assertIn("mypy-baseline filter", body) + self.assertNotIn("--allow-unsynced", body) + + def test_every_baseline_entry_names_a_file_that_exists(self): + stale = [] + for line in self.BASELINE.read_text(encoding="utf-8").splitlines(): + match = self.ENTRY.fullmatch(line) + self.assertIsNotNone(match, f"unreadable baseline entry: {line}") + if not (ROOT / match["path"]).is_file(): + stale.append(match["path"]) + self.assertEqual(stale, [], "baseline entries for files that are gone") + + def test_the_code_reports_no_error_outside_the_baseline(self): + for module in ("mypy", "mypy_baseline"): + try: + __import__(module) + except ImportError: + self.skipTest(f"{module} is not installed (pip install -e '.[dev]')") + report = subprocess.run( + [sys.executable, "-m", "mypy"], + cwd=ROOT, capture_output=True, text=True, check=False, + ) + result = subprocess.run( + [sys.executable, "-m", "mypy_baseline", "filter", "--no-colors"], + cwd=ROOT, input=report.stdout, capture_output=True, text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stdout) + + class TheProjectInstallsWithPip(unittest.TestCase): """pyproject declared a dev extra that pip could not install: without a [build-system] and an explicit module list, setuptools' flat-layout @@ -4991,9 +5112,34 @@ class VerifyAllSummaryNamesWhatIsWrong(unittest.TestCase): [(clean, ["RetroArch"]), (warned, ["RetroDECK"]), (core_miss, ["ROCKNIX"])] ) self.assertIn("1 warning (RetroDECK 1)", summary) - self.assertIn("ROCKNIX: n64.psh", summary) + self.assertIn("n64.psh (ROCKNIX)", summary) self.assertIn("0 critical,", summary) + def test_a_file_missing_on_several_platforms_is_counted_once(self): + """sfiii2a.zip missing on 15 platforms read '24 core file(s)' for 10 + distinct files, the name printed 15 times on one line.""" + sys.path.insert(0, str(ROOT / "scripts")) + import verify + + def missing(*names: str) -> dict: + return {"severity_counts": {}, "undeclared_files": [ + {"name": name, "required": True, "in_repo": False, + "category": "bios", "in_pack": True} for name in names + ]} + + summary = verify._overall_summary([ + (missing("sfiii2a.zip"), ["Batocera"]), + (missing("sfiii2a.zip", "ROM"), ["RetroPie"]), + (missing("sfiii2a.zip"), ["RomM"]), + (missing(), ["MinUI"]), + ]) + self.assertIn( + "2 core file(s) a core cannot run without are missing: " + "sfiii2a.zip (every platform but MinUI); ROM (RetroPie)", + summary, + ) + self.assertEqual(summary.count("sfiii2a.zip"), 1) + class OneOutputFormPerRun(unittest.TestCase): @@ -5956,6 +6102,20 @@ class TheRepairCommandWritesTheFileNamed(unittest.TestCase): load_database(target) self.assertIn(f"--output {target}", str(caught.exception)) + def test_a_typo_beside_the_database_names_the_database(self): + """--db databse.json beside database.json was told to rescan the whole + collection into a second file named after the typo.""" + from scripts.common import DatabaseUnreadable, load_database + + with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory: + (Path(directory) / "database.json").write_text("{}") + typo = Path(directory) / "databse.json" + with self.assertRaises(DatabaseUnreadable) as caught: + load_database(str(typo)) + message = str(caught.exception) + self.assertIn(f"Did you mean {typo.with_name('database.json')}?", message) + self.assertNotIn("generate_db.py", message) + class VerifyPacksRefusesWhatItCannotDo(unittest.TestCase): """The --verify-packs refusals lived in the --all-variants checks, which diff --git a/tests/test_autorun.py b/tests/test_autorun.py index ad15f7421..1600b99d1 100644 --- a/tests/test_autorun.py +++ b/tests/test_autorun.py @@ -7,6 +7,8 @@ a function returning canned CI runs. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_batocera_cores.py b/tests/test_batocera_cores.py index 159912e07..eeeff5203 100644 --- a/tests/test_batocera_cores.py +++ b/tests/test_batocera_cores.py @@ -9,6 +9,8 @@ under it: amiberry, fsuae and vice were never in a target list. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_batocera_md5_list.py b/tests/test_batocera_md5_list.py index 223d2add5..cc207dd7d 100644 --- a/tests/test_batocera_md5_list.py +++ b/tests/test_batocera_md5_list.py @@ -9,6 +9,8 @@ a list, and verify accepts any listed md5. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_bios_companions.py b/tests/test_bios_companions.py index 6f0bbe0d8..de9d9c45b 100644 --- a/tests/test_bios_companions.py +++ b/tests/test_bios_companions.py @@ -9,6 +9,8 @@ the Batocera BIOS folder. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_bios_layout.py b/tests/test_bios_layout.py index e447e43b9..5a9f73d66 100644 --- a/tests/test_bios_layout.py +++ b/tests/test_bios_layout.py @@ -7,6 +7,8 @@ Oric and Tangerine: one machine, two folders, two pages. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import unittest from pathlib import Path diff --git a/tests/test_bundled_identity.py b/tests/test_bundled_identity.py index 77eb121f1..d05fb9e18 100644 --- a/tests/test_bundled_identity.py +++ b/tests/test_bundled_identity.py @@ -13,6 +13,8 @@ entry is written with the hash of the file the emulator ships. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import unittest diff --git a/tests/test_cache_independent_counts.py b/tests/test_cache_independent_counts.py index 69a52ec15..62def66ae 100644 --- a/tests/test_cache_independent_counts.py +++ b/tests/test_cache_independent_counts.py @@ -10,6 +10,8 @@ directory cache is not a collection. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_campaign.py b/tests/test_campaign.py index 5966dd8d7..f1f41b7cb 100644 --- a/tests/test_campaign.py +++ b/tests/test_campaign.py @@ -10,6 +10,8 @@ resume and rerun on a changed core. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_case_insensitive_platforms.py b/tests/test_case_insensitive_platforms.py index 1eeb9be6c..168460fcc 100644 --- a/tests/test_case_insensitive_platforms.py +++ b/tests/test_case_insensitive_platforms.py @@ -7,6 +7,8 @@ went red. The flag lived in retrobat.yml and vanished at the next scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_casefold_cache.py b/tests/test_casefold_cache.py index 9105f9cac..3e6d50631 100644 --- a/tests/test_casefold_cache.py +++ b/tests/test_casefold_cache.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_check_freshness.py b/tests/test_check_freshness.py index 829062f66..0aa42c799 100644 --- a/tests/test_check_freshness.py +++ b/tests/test_check_freshness.py @@ -8,6 +8,8 @@ report is folded, since each of those decides whether a row says STALE. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import sys diff --git a/tests/test_check_release.py b/tests/test_check_release.py index 50a305f7c..c821b7bab 100644 --- a/tests/test_check_release.py +++ b/tests/test_check_release.py @@ -7,6 +7,8 @@ whose checksums every user's `sha256sum --check` rejected. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_checks_have_values.py b/tests/test_checks_have_values.py index 91c1fe841..8d9cea7a3 100644 --- a/tests/test_checks_have_values.py +++ b/tests/test_checks_have_values.py @@ -6,6 +6,8 @@ one. validate_schemas now refuses it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import unittest diff --git a/tests/test_cited_lines.py b/tests/test_cited_lines.py index bbf380f4f..1cf8794e5 100644 --- a/tests/test_cited_lines.py +++ b/tests/test_cited_lines.py @@ -7,6 +7,8 @@ does not hold the file. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_contributing_text.py b/tests/test_contributing_text.py index 593ad04fa..3e16e4838 100644 --- a/tests/test_contributing_text.py +++ b/tests/test_contributing_text.py @@ -6,6 +6,8 @@ the repository, five on the site, under two different titles. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_core_data_trees.py b/tests/test_core_data_trees.py index f9de142f5..2fb6e7f3e 100644 --- a/tests/test_core_data_trees.py +++ b/tests/test_core_data_trees.py @@ -10,6 +10,8 @@ reader now gets the tree, or the profile says its package installs it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_core_file_fills_declaration.py b/tests/test_core_file_fills_declaration.py index 1f9260057..499f67dd2 100644 --- a/tests/test_core_file_fills_declaration.py +++ b/tests/test_core_file_fills_declaration.py @@ -8,6 +8,8 @@ frontend sees a file at the path. verify kept it MISSING, so the pack said from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import contextlib import io diff --git a/tests/test_core_probe.py b/tests/test_core_probe.py index 60a8e1fc8..1c866ffc7 100644 --- a/tests/test_core_probe.py +++ b/tests/test_core_probe.py @@ -7,6 +7,8 @@ probe on it: staging, the worker process, strace, the record and its schema. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import contextlib import hashlib diff --git a/tests/test_core_size_refusal.py b/tests/test_core_size_refusal.py index 32f5615bc..8c4a437f6 100644 --- a/tests/test_core_size_refusal.py +++ b/tests/test_core_size_refusal.py @@ -10,6 +10,8 @@ read one rule (validation.core_size_refusal). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_coverage_cache.py b/tests/test_coverage_cache.py index 8c2b56175..8dd05038b 100644 --- a/tests/test_coverage_cache.py +++ b/tests/test_coverage_cache.py @@ -9,6 +9,8 @@ tests change an input and expect a fresh computation. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_cross_reference_sources.py b/tests/test_cross_reference_sources.py index 67de9cbfa..6f96e2d8e 100644 --- a/tests/test_cross_reference_sources.py +++ b/tests/test_cross_reference_sources.py @@ -9,6 +9,8 @@ a directory called ROM). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_crypto_aes.py b/tests/test_crypto_aes.py index fd399d15f..1e86433f1 100644 --- a/tests/test_crypto_aes.py +++ b/tests/test_crypto_aes.py @@ -7,6 +7,8 @@ contributor with none of them. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import sys import unittest diff --git a/tests/test_crypto_unique_data.py b/tests/test_crypto_unique_data.py index b2e2d6ae8..a7428f68f 100644 --- a/tests/test_crypto_unique_data.py +++ b/tests/test_crypto_unique_data.py @@ -18,6 +18,8 @@ Layouts, from Azahar src/core/hw/unique_data.cpp and src/core/file_sys/otp.cpp: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import struct import sys diff --git a/tests/test_crypto_verify.py b/tests/test_crypto_verify.py index 53e8e1ab7..a6f160bdd 100644 --- a/tests/test_crypto_verify.py +++ b/tests/test_crypto_verify.py @@ -14,6 +14,8 @@ constants rather than against itself. The AES vector is from NIST SP 800-38A. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import shutil import sys diff --git a/tests/test_data_dir_refs.py b/tests/test_data_dir_refs.py index df5d00bac..9793ea24d 100644 --- a/tests/test_data_dir_refs.py +++ b/tests/test_data_dir_refs.py @@ -6,6 +6,8 @@ verify only said "not cached" and refresh_data_dirs had nothing to fetch. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_dataclass_defaults.py b/tests/test_dataclass_defaults.py index 53019b75d..fd9fbca22 100644 --- a/tests/test_dataclass_defaults.py +++ b/tests/test_dataclass_defaults.py @@ -7,6 +7,8 @@ TypeError on the first loop. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import unittest from pathlib import Path diff --git a/tests/test_db_aliases.py b/tests/test_db_aliases.py index 1c456e65c..0c51d415f 100644 --- a/tests/test_db_aliases.py +++ b/tests/test_db_aliases.py @@ -8,6 +8,8 @@ disk ROM then served the CoCo one. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_declared_names.py b/tests/test_declared_names.py index 42868fe4b..630c60aa3 100644 --- a/tests/test_declared_names.py +++ b/tests/test_declared_names.py @@ -8,6 +8,8 @@ set, enriched with every spelling the database knows. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_dedup.py b/tests/test_dedup.py index d0badfbae..0b6d31790 100644 --- a/tests/test_dedup.py +++ b/tests/test_dedup.py @@ -9,6 +9,8 @@ are both load-bearing. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_derivations.py b/tests/test_derivations.py index 580bf32e5..061205b0b 100644 --- a/tests/test_derivations.py +++ b/tests/test_derivations.py @@ -7,6 +7,8 @@ is two held ROMs end to end. The gap report listed them as acquisition targets. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_deterministic_zip.py b/tests/test_deterministic_zip.py index 359f508c2..293f84b91 100644 --- a/tests/test_deterministic_zip.py +++ b/tests/test_deterministic_zip.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import tempfile diff --git a/tests/test_directory_entries.py b/tests/test_directory_entries.py index e8472f32f..3bd952006 100644 --- a/tests/test_directory_entries.py +++ b/tests/test_directory_entries.py @@ -9,6 +9,8 @@ reported missing while 3 584 collected files stayed out of every pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import sys diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index cdd6c3ed1..dbdf0c8b5 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -7,6 +7,8 @@ code moves, and nothing else notices. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import hashlib import inspect diff --git a/tests/test_doc_surfaces.py b/tests/test_doc_surfaces.py index b7a53ee06..75daca3d0 100644 --- a/tests/test_doc_surfaces.py +++ b/tests/test_doc_surfaces.py @@ -9,6 +9,8 @@ tool prints, and a size is the one the release record holds. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import contextlib import re diff --git a/tests/test_download.py b/tests/test_download.py index 154f780ca..8923a2134 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -9,6 +9,8 @@ checksum of each layout and the staging location. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 894b43a44..d876b56d4 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -17,6 +17,8 @@ Covers: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_elec_scrapers.py b/tests/test_elec_scrapers.py index aeace915c..0ee2f1950 100644 --- a/tests/test_elec_scrapers.py +++ b/tests/test_elec_scrapers.py @@ -9,6 +9,8 @@ upstream files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_emuelec_targets.py b/tests/test_emuelec_targets.py index c8f911433..77e8b2ec1 100644 --- a/tests/test_emuelec_targets.py +++ b/tests/test_emuelec_targets.py @@ -9,6 +9,8 @@ firmware of cores its image never had. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_emulator_pack_counts.py b/tests/test_emulator_pack_counts.py index 10faa9154..654ff5c20 100644 --- a/tests/test_emulator_pack_counts.py +++ b/tests/test_emulator_pack_counts.py @@ -9,6 +9,8 @@ names and ten "missing". from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import subprocess import sys diff --git a/tests/test_entry_categories.py b/tests/test_entry_categories.py index 0f971c5d9..bd1e849d2 100644 --- a/tests/test_entry_categories.py +++ b/tests/test_entry_categories.py @@ -6,6 +6,8 @@ from what the cores the platform runs open, never from the entry's name. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index f0abfc261..79734c9bd 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -9,6 +9,8 @@ from a file other than the one the hash describes. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_export_native_layer.py b/tests/test_export_native_layer.py index 6e93642d5..7c9446218 100644 --- a/tests/test_export_native_layer.py +++ b/tests/test_export_native_layer.py @@ -9,6 +9,8 @@ libretro's System.dat (N88EXT0-3.ROM) as if libretro declared them. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_export_report.py b/tests/test_export_report.py index 1b25ab45e..e3127fe9f 100644 --- a/tests/test_export_report.py +++ b/tests/test_export_report.py @@ -7,6 +7,8 @@ file from and no list to check the diff against. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import tempfile diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 8dc24b4ff..ccd275ccf 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -14,6 +14,8 @@ and skip when it has not been populated (python scripts/export_native.py from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import difflib import io import json diff --git a/tests/test_extra_identity.py b/tests/test_extra_identity.py index a0761e39c..1f1faca65 100644 --- a/tests/test_extra_identity.py +++ b/tests/test_extra_identity.py @@ -8,6 +8,8 @@ Galaksija ROM and a PS2 ROM2.BIN. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_extract_targets.py b/tests/test_extract_targets.py index 9ba78bf4b..59c8022f8 100644 --- a/tests/test_extract_targets.py +++ b/tests/test_extract_targets.py @@ -9,6 +9,8 @@ have vanished with it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_fbneo_parser.py b/tests/test_fbneo_parser.py index 4a16c2c2b..583e75140 100644 --- a/tests/test_fbneo_parser.py +++ b/tests/test_fbneo_parser.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import tempfile import unittest from pathlib import Path diff --git a/tests/test_file_consumers.py b/tests/test_file_consumers.py index 1d382916f..87b2495ee 100644 --- a/tests/test_file_consumers.py +++ b/tests/test_file_consumers.py @@ -8,6 +8,8 @@ homonym tests/test_game_data_homonyms.py forbids the resolver to serve. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_fileless_audit.py b/tests/test_fileless_audit.py index 7d469ab4f..8409bb19c 100644 --- a/tests/test_fileless_audit.py +++ b/tests/test_fileless_audit.py @@ -8,6 +8,8 @@ directory, and only a manual reading found it. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_finalize_pack_names.py b/tests/test_finalize_pack_names.py index fe7c8e686..7dff70d1c 100644 --- a/tests/test_finalize_pack_names.py +++ b/tests/test_finalize_pack_names.py @@ -7,6 +7,8 @@ attached to misterfpga by substring and checked against the full list: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_find_pinned_revision.py b/tests/test_find_pinned_revision.py index 473633fd5..974010a55 100644 --- a/tests/test_find_pinned_revision.py +++ b/tests/test_find_pinned_revision.py @@ -6,6 +6,8 @@ today's upstream no longer carries, but the bytes are in the project's history. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import unittest diff --git a/tests/test_flycast_layout.py b/tests/test_flycast_layout.py index 81e60b699..3bbd01358 100644 --- a/tests/test_flycast_layout.py +++ b/tests/test_flycast_layout.py @@ -9,6 +9,8 @@ and put every file one directory too deep. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import unittest from pathlib import Path diff --git a/tests/test_foreign_tree.py b/tests/test_foreign_tree.py index 47056e553..b8260e8af 100644 --- a/tests/test_foreign_tree.py +++ b/tests/test_foreign_tree.py @@ -9,6 +9,8 @@ to upstream PCSX2. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_foreign_tree_names.py b/tests/test_foreign_tree_names.py index 360060add..258401f6b 100644 --- a/tests/test_foreign_tree_names.py +++ b/tests/test_foreign_tree_names.py @@ -9,6 +9,8 @@ served while data/bluemsx held the 8 KB one. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_frontend_digest.py b/tests/test_frontend_digest.py index 14b260385..a4ce6a5cd 100644 --- a/tests/test_frontend_digest.py +++ b/tests/test_frontend_digest.py @@ -8,6 +8,8 @@ the pack and the manifest while verify, comparing md5, called it OK. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_game_catalogs.py b/tests/test_game_catalogs.py index 306d78844..ef3873ed3 100644 --- a/tests/test_game_catalogs.py +++ b/tests/test_game_catalogs.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import subprocess import sys diff --git a/tests/test_game_data_homonyms.py b/tests/test_game_data_homonyms.py index 6e5cc144f..c39d95e43 100644 --- a/tests/test_game_data_homonyms.py +++ b/tests/test_game_data_homonyms.py @@ -12,6 +12,8 @@ carries `unsourceable:` until its own bytes are held. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import re diff --git a/tests/test_game_headers.py b/tests/test_game_headers.py index 6a3c63087..1b495bb66 100644 --- a/tests/test_game_headers.py +++ b/tests/test_game_headers.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_game_regions.py b/tests/test_game_regions.py index 97f660eae..b3b76cf6d 100644 --- a/tests/test_game_regions.py +++ b/tests/test_game_regions.py @@ -8,6 +8,8 @@ can lose its backing, then on the repository's own files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import copy import json import sys diff --git a/tests/test_games_index.py b/tests/test_games_index.py index b7894b42f..07c05e459 100644 --- a/tests/test_games_index.py +++ b/tests/test_games_index.py @@ -7,6 +7,8 @@ so a test pins the join and not the state of the data. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import shutil import sys diff --git a/tests/test_gap_settlement.py b/tests/test_gap_settlement.py index 683044985..917507662 100644 --- a/tests/test_gap_settlement.py +++ b/tests/test_gap_settlement.py @@ -7,6 +7,8 @@ it. RetroArch names Galaksija's 4 KiB galaksija/rom1.bin, so DOSBox's from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_handheld_launchers.py b/tests/test_handheld_launchers.py index ff8d793d9..633d6259a 100644 --- a/tests/test_handheld_launchers.py +++ b/tests/test_handheld_launchers.py @@ -10,6 +10,8 @@ the pad reaching the console, a compositor covering it) is not exercised. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import importlib.util import json diff --git a/tests/test_hash_merge.py b/tests/test_hash_merge.py index 079dfe4e5..8b63de4b3 100644 --- a/tests/test_hash_merge.py +++ b/tests/test_hash_merge.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import tempfile import unittest diff --git a/tests/test_hashing.py b/tests/test_hashing.py index d6c6b71e8..6237e4330 100644 --- a/tests/test_hashing.py +++ b/tests/test_hashing.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import shutil import sys import tempfile diff --git a/tests/test_hidden_extras.py b/tests/test_hidden_extras.py index 222b018fa..452c3205c 100644 --- a/tests/test_hidden_extras.py +++ b/tests/test_hidden_extras.py @@ -9,6 +9,8 @@ ROCKNIX's higan SGB1.sfc/program.rom under the file SGB1.sfc. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_identity.py b/tests/test_identity.py index a90f9796b..fce2c7ca9 100644 --- a/tests/test_identity.py +++ b/tests/test_identity.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import shutil import sys import tempfile diff --git a/tests/test_install.py b/tests/test_install.py index 086d9b697..9effcdd49 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1,6 +1,8 @@ """Tests for install.py platform detection and config parsing.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_install_app.py b/tests/test_install_app.py index 94d190236..34a4e499d 100644 --- a/tests/test_install_app.py +++ b/tests/test_install_app.py @@ -8,6 +8,8 @@ menu) is not exercised. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_install_cores.py b/tests/test_install_cores.py index 55fcbfbf0..7594890fe 100644 --- a/tests/test_install_cores.py +++ b/tests/test_install_cores.py @@ -8,6 +8,8 @@ which cores keep each system, the rule a pack built for a target follows. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 2ea164a30..840b948e2 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -7,6 +7,8 @@ honours Retry-After, and large files that resume where they stopped. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import errno import hashlib diff --git a/tests/test_install_from.py b/tests/test_install_from.py index ff28c52c7..9b91ed553 100644 --- a/tests/test_install_from.py +++ b/tests/test_install_from.py @@ -11,6 +11,8 @@ never moved, and checked again once written. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import copy import errno diff --git a/tests/test_install_games.py b/tests/test_install_games.py index 117970364..8388202d0 100644 --- a/tests/test_install_games.py +++ b/tests/test_install_games.py @@ -8,6 +8,8 @@ index, published by scripts/generate_games_index.py. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import gzip diff --git a/tests/test_install_offline.py b/tests/test_install_offline.py index 08d9194d2..2eeb18530 100644 --- a/tests/test_install_offline.py +++ b/tests/test_install_offline.py @@ -9,6 +9,8 @@ installs again from disk, --from-pack, every file checked by its hash. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index d0cc9dac3..d96329ae7 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -9,6 +9,8 @@ to one answer. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 988b7234e..a7584e71b 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -9,6 +9,8 @@ installs it). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import contextlib import errno diff --git a/tests/test_lakka_targets.py b/tests/test_lakka_targets.py index 387ac1502..e4fb1cb31 100644 --- a/tests/test_lakka_targets.py +++ b/tests/test_lakka_targets.py @@ -7,6 +7,8 @@ platform outright. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import unittest from scripts.scraper.targets import lakka_targets_scraper as lakka diff --git a/tests/test_large_file_cache.py b/tests/test_large_file_cache.py index 909ede2cf..bd4dd01d5 100644 --- a/tests/test_large_file_cache.py +++ b/tests/test_large_file_cache.py @@ -8,6 +8,8 @@ that then replaced the cache entry. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_launcher_aliases.py b/tests/test_launcher_aliases.py index bd4d5331d..ab7795a02 100644 --- a/tests/test_launcher_aliases.py +++ b/tests/test_launcher_aliases.py @@ -16,6 +16,8 @@ Two checks read the committed data: from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from collections import defaultdict diff --git a/tests/test_launcher_layer.py b/tests/test_launcher_layer.py index e765fe27e..a7f99c230 100644 --- a/tests/test_launcher_layer.py +++ b/tests/test_launcher_layer.py @@ -8,6 +8,8 @@ and a fixed path the launcher passes (Batocera's x16emu shape). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import inspect import os diff --git a/tests/test_launchers.py b/tests/test_launchers.py index 515d242ee..12a7d06ff 100644 --- a/tests/test_launchers.py +++ b/tests/test_launchers.py @@ -8,6 +8,8 @@ is checked against the revision itself. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import dataclasses import json import sys diff --git a/tests/test_launchers_emudeck.py b/tests/test_launchers_emudeck.py index 33d7596fe..1dd106f6c 100644 --- a/tests/test_launchers_emudeck.py +++ b/tests/test_launchers_emudeck.py @@ -8,6 +8,8 @@ fixture's own lines, and a construct that is gone must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_launchers_recalbox.py b/tests/test_launchers_recalbox.py index 5c806f757..381920bc9 100644 --- a/tests/test_launchers_recalbox.py +++ b/tests/test_launchers_recalbox.py @@ -8,6 +8,8 @@ is checked against the tag itself. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_launchers_retrobat.py b/tests/test_launchers_retrobat.py index 4a5d80fa6..4d1591452 100644 --- a/tests/test_launchers_retrobat.py +++ b/tests/test_launchers_retrobat.py @@ -8,6 +8,8 @@ the fixture's own lines, and a construct that is gone must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_launchers_retrodeck.py b/tests/test_launchers_retrodeck.py index a4af6844f..96401837c 100644 --- a/tests/test_launchers_retrodeck.py +++ b/tests/test_launchers_retrodeck.py @@ -8,6 +8,8 @@ cites. A construct that is gone or unknown must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_launchers_retropie.py b/tests/test_launchers_retropie.py index 1412ae5e0..0e5ef9c66 100644 --- a/tests/test_launchers_retropie.py +++ b/tests/test_launchers_retropie.py @@ -8,6 +8,8 @@ cites. A construct that is gone or unknown must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import dataclasses import json import sys diff --git a/tests/test_launchers_rocknix.py b/tests/test_launchers_rocknix.py index b49097ae3..22596256b 100644 --- a/tests/test_launchers_rocknix.py +++ b/tests/test_launchers_rocknix.py @@ -9,6 +9,8 @@ configs differ, and a construct that is gone must stop the scrape. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_libretro_layout.py b/tests/test_libretro_layout.py index 76b089802..6b0aa29fb 100644 --- a/tests/test_libretro_layout.py +++ b/tests/test_libretro_layout.py @@ -9,6 +9,8 @@ Sys from system/dolphin-emu/Sys and keeps User in the save directory. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_libretro_slugs.py b/tests/test_libretro_slugs.py index 1b52ce124..1d571b002 100644 --- a/tests/test_libretro_slugs.py +++ b/tests/test_libretro_slugs.py @@ -9,6 +9,8 @@ duplicate system carrying the quasi88 group. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_maker_folder_names.py b/tests/test_maker_folder_names.py index 8410ac58e..cffe035af 100644 --- a/tests/test_maker_folder_names.py +++ b/tests/test_maker_folder_names.py @@ -11,6 +11,8 @@ remains. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_mame_devices.py b/tests/test_mame_devices.py index 4b34edde0..05aa8f36e 100644 --- a/tests/test_mame_devices.py +++ b/tests/test_mame_devices.py @@ -9,6 +9,8 @@ filed under the systems of the machines that can load it, never guessed. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import tempfile diff --git a/tests/test_mame_parser.py b/tests/test_mame_parser.py index 288e2afd8..c163e845b 100644 --- a/tests/test_mame_parser.py +++ b/tests/test_mame_parser.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import tempfile import unittest diff --git a/tests/test_mame_ref_audit.py b/tests/test_mame_ref_audit.py index 69d60ae44..7f6113739 100644 --- a/tests/test_mame_ref_audit.py +++ b/tests/test_mame_ref_audit.py @@ -7,6 +7,8 @@ found nineteen stale refs in one driver where profile_sync had flagged five. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_manifest_entry.py b/tests/test_manifest_entry.py index 3505bbf26..ad18dfe2d 100644 --- a/tests/test_manifest_entry.py +++ b/tests/test_manifest_entry.py @@ -9,6 +9,8 @@ an empty repo_path, and the installer refused the whole manifest. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import json import sys diff --git a/tests/test_md5_pack_context.py b/tests/test_md5_pack_context.py index d3b572206..bac9ef345 100644 --- a/tests/test_md5_pack_context.py +++ b/tests/test_md5_pack_context.py @@ -8,6 +8,8 @@ the bare file names under a pack called Standalone. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import subprocess import sys diff --git a/tests/test_measurements.py b/tests/test_measurements.py index 692b409f3..e38e3ad2a 100644 --- a/tests/test_measurements.py +++ b/tests/test_measurements.py @@ -8,6 +8,8 @@ against HEAD. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import copy import html import json diff --git a/tests/test_minui_platform.py b/tests/test_minui_platform.py index 8807c22f4..5e6ed3ef3 100644 --- a/tests/test_minui_platform.py +++ b/tests/test_minui_platform.py @@ -11,6 +11,8 @@ profiles, placed below that folder. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_name_tiebreak.py b/tests/test_name_tiebreak.py index 29cd9566b..9270a30c2 100644 --- a/tests/test_name_tiebreak.py +++ b/tests/test_name_tiebreak.py @@ -7,6 +7,8 @@ without `validation: [size]` keeps its informative status. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_native_mode.py b/tests/test_native_mode.py index 100412e98..51bf58895 100644 --- a/tests/test_native_mode.py +++ b/tests/test_native_mode.py @@ -10,6 +10,8 @@ it, and these tests make the agreement checkable instead of reviewable. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_nec_filing.py b/tests/test_nec_filing.py index 68335af0d..329cf7f08 100644 --- a/tests/test_nec_filing.py +++ b/tests/test_nec_filing.py @@ -9,6 +9,8 @@ no PC-88 emulator reads. Reads the repository. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_no_case_collisions.py b/tests/test_no_case_collisions.py index c9821676c..a724cfca2 100644 --- a/tests/test_no_case_collisions.py +++ b/tests/test_no_case_collisions.py @@ -1,5 +1,7 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import unittest from collections import defaultdict diff --git a/tests/test_no_game_data.py b/tests/test_no_game_data.py index 54e835c57..2bb59c2fc 100644 --- a/tests/test_no_game_data.py +++ b/tests/test_no_game_data.py @@ -9,6 +9,8 @@ full manifest. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import functools import hashlib diff --git a/tests/test_np2_fonts.py b/tests/test_np2_fonts.py index fe417bff0..0ff2196dd 100644 --- a/tests/test_np2_fonts.py +++ b/tests/test_np2_fonts.py @@ -9,6 +9,8 @@ which fontpc98_read cannot parse, and FONT.ROM was never placed under its name. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_optional_dependencies.py b/tests/test_optional_dependencies.py index 1ef0b7a41..feba5a1ee 100644 --- a/tests/test_optional_dependencies.py +++ b/tests/test_optional_dependencies.py @@ -7,6 +7,8 @@ fifteen errors instead of skipped schema checks. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import unittest diff --git a/tests/test_owner_resolution.py b/tests/test_owner_resolution.py index f135bd1fa..70141c51a 100644 --- a/tests/test_owner_resolution.py +++ b/tests/test_owner_resolution.py @@ -9,6 +9,8 @@ armsx2's pack ships without a reader, and MAME's neogeo.zip without MAME. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_pack_conformance_dirs.py b/tests/test_pack_conformance_dirs.py index f17d77eb0..659564f6c 100644 --- a/tests/test_pack_conformance_dirs.py +++ b/tests/test_pack_conformance_dirs.py @@ -8,6 +8,8 @@ all and never judged. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import sys import tempfile diff --git a/tests/test_pack_core_extras.py b/tests/test_pack_core_extras.py index 266763d14..9e659369b 100644 --- a/tests/test_pack_core_extras.py +++ b/tests/test_pack_core_extras.py @@ -9,6 +9,8 @@ builder's in the same run. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_pack_counts.py b/tests/test_pack_counts.py index 1d12c5423..55f34e029 100644 --- a/tests/test_pack_counts.py +++ b/tests/test_pack_counts.py @@ -10,6 +10,8 @@ installer fetches and leaves out the data directories a pack also carries. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_pack_destinations.py b/tests/test_pack_destinations.py index 65bd1277d..cf41ce6a3 100644 --- a/tests/test_pack_destinations.py +++ b/tests/test_pack_destinations.py @@ -9,6 +9,8 @@ it and counted one more file from the cores. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_pack_failures.py b/tests/test_pack_failures.py index 5e73ac684..9ec1e7d1d 100644 --- a/tests/test_pack_failures.py +++ b/tests/test_pack_failures.py @@ -7,6 +7,8 @@ keep serving from an older run, left the run green. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import os import sys diff --git a/tests/test_pack_file_names.py b/tests/test_pack_file_names.py index 30bea04c0..cf5c4de43 100644 --- a/tests/test_pack_file_names.py +++ b/tests/test_pack_file_names.py @@ -7,6 +7,8 @@ verify reported the same scope OK. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_pack_grouping.py b/tests/test_pack_grouping.py index d24674c4a..5a45a6740 100644 --- a/tests/test_pack_grouping.py +++ b/tests/test_pack_grouping.py @@ -8,6 +8,8 @@ ruled undecidable, and the keep overrode the drop. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_pack_integrity.py b/tests/test_pack_integrity.py index 57bc09dc1..a7e7c704e 100644 --- a/tests/test_pack_integrity.py +++ b/tests/test_pack_integrity.py @@ -14,6 +14,8 @@ date pack stays a failure. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import glob import os import subprocess diff --git a/tests/test_pack_member_identity.py b/tests/test_pack_member_identity.py index 63d15f47e..5eeb7d64b 100644 --- a/tests/test_pack_member_identity.py +++ b/tests/test_pack_member_identity.py @@ -8,6 +8,8 @@ recognised by their members. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import io import json diff --git a/tests/test_pack_probe.py b/tests/test_pack_probe.py index de96601cd..3d43ff2c4 100644 --- a/tests/test_pack_probe.py +++ b/tests/test_pack_probe.py @@ -11,6 +11,8 @@ on disk. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_pack_readme.py b/tests/test_pack_readme.py index 5a56085cf..83748d59c 100644 --- a/tests/test_pack_readme.py +++ b/tests/test_pack_readme.py @@ -8,6 +8,8 @@ does not have, while the registry names the path. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_path_conflict_status.py b/tests/test_path_conflict_status.py index 5d7c60690..653064812 100644 --- a/tests/test_path_conflict_status.py +++ b/tests/test_path_conflict_status.py @@ -8,6 +8,8 @@ from the collection read as covered in the pack report and as missing in verify. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_path_null_copies.py b/tests/test_path_null_copies.py index eb2a405ea..d3b583c5e 100644 --- a/tests/test_path_null_copies.py +++ b/tests/test_path_null_copies.py @@ -8,6 +8,8 @@ Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_path_suffix.py b/tests/test_path_suffix.py index 865996528..62ee65d56 100644 --- a/tests/test_path_suffix.py +++ b/tests/test_path_suffix.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import shutil import sys diff --git a/tests/test_pin_build.py b/tests/test_pin_build.py index c4f7a4568..ecd3731bc 100644 --- a/tests/test_pin_build.py +++ b/tests/test_pin_build.py @@ -8,6 +8,8 @@ file, and check_freshness reads it again where the platform now stands. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_platform_inheritance.py b/tests/test_platform_inheritance.py index 10473c882..d4ba642f7 100644 --- a/tests/test_platform_inheritance.py +++ b/tests/test_platform_inheritance.py @@ -8,6 +8,8 @@ scraper and install block. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import shutil import sys import tempfile diff --git a/tests/test_platform_keys.py b/tests/test_platform_keys.py index c11b8788e..95be6420a 100644 --- a/tests/test_platform_keys.py +++ b/tests/test_platform_keys.py @@ -8,6 +8,8 @@ counted as declared: 76 Batocera entries added names such as boot0.rom. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_port.py b/tests/test_port.py index 5c873e84b..afb8662c7 100644 --- a/tests/test_port.py +++ b/tests/test_port.py @@ -8,6 +8,8 @@ route of the port, and the other launchers, are in test_handheld_launchers. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import importlib.util import io diff --git a/tests/test_profile_checks.py b/tests/test_profile_checks.py index e4fb44c97..a7de1e04a 100644 --- a/tests/test_profile_checks.py +++ b/tests/test_profile_checks.py @@ -9,6 +9,8 @@ check left out lets through a file the emulator refuses. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import tempfile diff --git a/tests/test_profile_names.py b/tests/test_profile_names.py index c5ec1a778..da172b185 100644 --- a/tests/test_profile_names.py +++ b/tests/test_profile_names.py @@ -7,6 +7,8 @@ own name that way. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_profile_paths.py b/tests/test_profile_paths.py index 3df44b9ce..eea16aea9 100644 --- a/tests/test_profile_paths.py +++ b/tests/test_profile_paths.py @@ -8,6 +8,8 @@ app's own code joins that folder to its data root. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import unittest from pathlib import Path diff --git a/tests/test_profile_repositories.py b/tests/test_profile_repositories.py index c00f94a83..76e63fe00 100644 --- a/tests/test_profile_repositories.py +++ b/tests/test_profile_repositories.py @@ -11,6 +11,8 @@ and skips. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 95879c00a..7724ad0d8 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import contextlib import io diff --git a/tests/test_prose_style.py b/tests/test_prose_style.py index d089e9f45..9081f77f4 100644 --- a/tests/test_prose_style.py +++ b/tests/test_prose_style.py @@ -8,6 +8,8 @@ not write them. This module names every place one comes back. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import os import re diff --git a/tests/test_provenance.py b/tests/test_provenance.py index d76d3a9b4..4dd467c74 100644 --- a/tests/test_provenance.py +++ b/tests/test_provenance.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import tempfile import unittest diff --git a/tests/test_refresh_data_dirs.py b/tests/test_refresh_data_dirs.py index b7fcd5617..604eba7ba 100644 --- a/tests/test_refresh_data_dirs.py +++ b/tests/test_refresh_data_dirs.py @@ -9,6 +9,8 @@ empty JSON document. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import sys diff --git a/tests/test_refresh_failures.py b/tests/test_refresh_failures.py index 11e2bfc27..7e2942913 100644 --- a/tests/test_refresh_failures.py +++ b/tests/test_refresh_failures.py @@ -8,6 +8,8 @@ failed, the cache left at the old version. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import shutil diff --git a/tests/test_refresh_stale.py b/tests/test_refresh_stale.py index e92887a66..c6916358b 100644 --- a/tests/test_refresh_stale.py +++ b/tests/test_refresh_stale.py @@ -8,6 +8,8 @@ by the refresh of the original its export patches. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_region.py b/tests/test_region.py index d0611a975..208c5bdd0 100644 --- a/tests/test_region.py +++ b/tests/test_region.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_region_agnostic_scan.py b/tests/test_region_agnostic_scan.py index 35175f38a..f6a7eda39 100644 --- a/tests/test_region_agnostic_scan.py +++ b/tests/test_region_agnostic_scan.py @@ -9,6 +9,8 @@ ROCKNIX. Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_region_check.py b/tests/test_region_check.py index 83e924129..5fd073ada 100644 --- a/tests/test_region_check.py +++ b/tests/test_region_check.py @@ -3,6 +3,8 @@ records settle.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_region_groups.py b/tests/test_region_groups.py index 6330b1aca..4ac1a0c66 100644 --- a/tests/test_region_groups.py +++ b/tests/test_region_groups.py @@ -7,6 +7,8 @@ Enterprise group drop the German Enterprise ROM under --region us. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_region_held.py b/tests/test_region_held.py index cb384cb44..3c50a8754 100644 --- a/tests/test_region_held.py +++ b/tests/test_region_held.py @@ -8,6 +8,8 @@ Japanese one, and the system shipped no BIOS at all. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import shutil diff --git a/tests/test_region_platform_index.py b/tests/test_region_platform_index.py index fbb1a59ea..12d206dd3 100644 --- a/tests/test_region_platform_index.py +++ b/tests/test_region_platform_index.py @@ -8,6 +8,8 @@ voided the [europe] tag Genesis Plus GX and PicoDrive give it, and from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_region_required_only.py b/tests/test_region_required_only.py index f361b33b7..b3a2cfdef 100644 --- a/tests/test_region_required_only.py +++ b/tests/test_region_required_only.py @@ -8,6 +8,8 @@ the optional winner: the slot shipped empty. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import sys import unittest diff --git a/tests/test_release_packs.py b/tests/test_release_packs.py index a32e3a2a9..e3d54147a 100644 --- a/tests/test_release_packs.py +++ b/tests/test_release_packs.py @@ -13,6 +13,8 @@ platform has a permanent page at get// instead. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import json diff --git a/tests/test_required_only_archives.py b/tests/test_required_only_archives.py index c8866e41b..417d642db 100644 --- a/tests/test_required_only_archives.py +++ b/tests/test_required_only_archives.py @@ -6,6 +6,8 @@ its _Required pack held them anyway: the archive loop never read `required`. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import tempfile diff --git a/tests/test_required_only_twins.py b/tests/test_required_only_twins.py index c985096a9..cb08788fd 100644 --- a/tests/test_required_only_twins.py +++ b/tests/test_required_only_twins.py @@ -11,6 +11,8 @@ the manifest without a trace. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_resolution_evidence.py b/tests/test_resolution_evidence.py index 588a18be4..2d2c2c4e1 100644 --- a/tests/test_resolution_evidence.py +++ b/tests/test_resolution_evidence.py @@ -10,6 +10,8 @@ predicate is that check. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_retired_files.py b/tests/test_retired_files.py index b6387f1d3..fee66c0ab 100644 --- a/tests/test_retired_files.py +++ b/tests/test_retired_files.py @@ -8,6 +8,8 @@ contribution of a file the collection had held. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import tempfile diff --git a/tests/test_retrodeck_scraper_errors.py b/tests/test_retrodeck_scraper_errors.py index 2ab272d9d..7d2f2b282 100644 --- a/tests/test_retrodeck_scraper_errors.py +++ b/tests/test_retrodeck_scraper_errors.py @@ -7,6 +7,8 @@ PS2 BIOS and the pcsx2 standalone core, exit code 0. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import sys diff --git a/tests/test_romfolders_card.py b/tests/test_romfolders_card.py index 2a106976e..615cd941d 100644 --- a/tests/test_romfolders_card.py +++ b/tests/test_romfolders_card.py @@ -7,6 +7,8 @@ schema and their own declared sources. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_romfolders_es.py b/tests/test_romfolders_es.py index 828966f63..e3aa43084 100644 --- a/tests/test_romfolders_es.py +++ b/tests/test_romfolders_es.py @@ -9,6 +9,8 @@ naming a declared source, and a second dump giving the same bytes. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_romm_hash_rule.py b/tests/test_romm_hash_rule.py index 922d83f9f..17ec065e8 100644 --- a/tests/test_romm_hash_rule.py +++ b/tests/test_romm_hash_rule.py @@ -8,6 +8,8 @@ a wrong declared size passed here and failed there. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import sys diff --git a/tests/test_romset_recipes.py b/tests/test_romset_recipes.py index 88c666ce2..769345f22 100644 --- a/tests/test_romset_recipes.py +++ b/tests/test_romset_recipes.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import sys diff --git a/tests/test_save_root.py b/tests/test_save_root.py index e0a58c5ea..67a2a42d1 100644 --- a/tests/test_save_root.py +++ b/tests/test_save_root.py @@ -11,6 +11,8 @@ directory the frontend's config names. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import functools import hashlib import http.server diff --git a/tests/test_schema_semantic_gate.py b/tests/test_schema_semantic_gate.py index 9ccc9692c..5d6fd9983 100644 --- a/tests/test_schema_semantic_gate.py +++ b/tests/test_schema_semantic_gate.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import io import json import sys diff --git a/tests/test_scraper_contract.py b/tests/test_scraper_contract.py index cf9e04c20..9615e9ebd 100644 --- a/tests/test_scraper_contract.py +++ b/tests/test_scraper_contract.py @@ -10,6 +10,8 @@ exercised is a promise to contributors backed by nothing. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import importlib import io import json diff --git a/tests/test_sega_filing.py b/tests/test_sega_filing.py index 0fd10af1e..939fe0d40 100644 --- a/tests/test_sega_filing.py +++ b/tests/test_sega_filing.py @@ -9,6 +9,8 @@ Reads the repository. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_shared_groups.py b/tests/test_shared_groups.py index 94c0219ec..f418b570b 100644 --- a/tests/test_shared_groups.py +++ b/tests/test_shared_groups.py @@ -9,6 +9,8 @@ it back as a required file of RetroArch, Lakka and RetroPie. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_shipped_content.py b/tests/test_shipped_content.py index f7beb3708..2e49ee6c8 100644 --- a/tests/test_shipped_content.py +++ b/tests/test_shipped_content.py @@ -15,6 +15,8 @@ path each entry comes from. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_site_arcade_boards.py b/tests/test_site_arcade_boards.py index 6f836cd8a..0f22664c4 100644 --- a/tests/test_site_arcade_boards.py +++ b/tests/test_site_arcade_boards.py @@ -11,6 +11,8 @@ link or carry their reason below. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import tempfile import unittest diff --git a/tests/test_site_availability.py b/tests/test_site_availability.py index 12fa9052c..46efd86b6 100644 --- a/tests/test_site_availability.py +++ b/tests/test_site_availability.py @@ -8,6 +8,8 @@ held across the site while the gaps page listed them as known absences. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_site_bootstrap.py b/tests/test_site_bootstrap.py index ee41e51bb..3b736cad1 100644 --- a/tests/test_site_bootstrap.py +++ b/tests/test_site_bootstrap.py @@ -9,6 +9,8 @@ that long, so each copy reads install.py from the commit it was built from. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_site_built_gate.py b/tests/test_site_built_gate.py index c1957ab2f..1734cf873 100644 --- a/tests/test_site_built_gate.py +++ b/tests/test_site_built_gate.py @@ -15,6 +15,8 @@ Skips when the site is not built. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_components.py b/tests/test_site_components.py index 376724742..ee73fbb92 100644 --- a/tests/test_site_components.py +++ b/tests/test_site_components.py @@ -18,6 +18,8 @@ node and skip without it. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import shutil diff --git a/tests/test_site_console_folders.py b/tests/test_site_console_folders.py index 514667a43..9d3a895ce 100644 --- a/tests/test_site_console_folders.py +++ b/tests/test_site_console_folders.py @@ -12,6 +12,8 @@ three samples committed under "FBAlpha 2012" and four release assets under from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import subprocess diff --git a/tests/test_site_content_data.py b/tests/test_site_content_data.py index a9c0f2d40..e95382dc1 100644 --- a/tests/test_site_content_data.py +++ b/tests/test_site_content_data.py @@ -8,6 +8,8 @@ needs no Python change, and a mistake stops the build with its name. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import re import shutil diff --git a/tests/test_site_copy.py b/tests/test_site_copy.py index 47507aa5f..5d2bb9662 100644 --- a/tests/test_site_copy.py +++ b/tests/test_site_copy.py @@ -12,6 +12,8 @@ print the measures of every page held here. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import re import statistics diff --git a/tests/test_site_cores.py b/tests/test_site_cores.py index 16f42673a..9bc8c3d70 100644 --- a/tests/test_site_cores.py +++ b/tests/test_site_cores.py @@ -7,6 +7,8 @@ where its pack is built from 321. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_site_css.py b/tests/test_site_css.py index 99cf0132a..e9dde975b 100644 --- a/tests/test_site_css.py +++ b/tests/test_site_css.py @@ -25,6 +25,8 @@ crumb separator free to end a line alone. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from pathlib import Path diff --git a/tests/test_site_density_cards.py b/tests/test_site_density_cards.py index 6bea1ba41..c932bcc08 100644 --- a/tests/test_site_density_cards.py +++ b/tests/test_site_density_cards.py @@ -10,6 +10,8 @@ sits in closed folds, every fact still on the page. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import types diff --git a/tests/test_site_emulator_page.py b/tests/test_site_emulator_page.py index be1165ee1..9ffcb5842 100644 --- a/tests/test_site_emulator_page.py +++ b/tests/test_site_emulator_page.py @@ -5,6 +5,8 @@ No network and no database: pages are rendered from small profiles. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_site_emulators.py b/tests/test_site_emulators.py index e8421df90..102eef7ac 100644 --- a/tests/test_site_emulators.py +++ b/tests/test_site_emulators.py @@ -27,6 +27,8 @@ Linux, Windows and Android commands, for MAME4droid an Android-only app. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_site_entry.py b/tests/test_site_entry.py index 3806438ba..bb9acc096 100644 --- a/tests/test_site_entry.py +++ b/tests/test_site_entry.py @@ -32,6 +32,8 @@ Linux handheld rows sent the reader to a menu. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_entry_tables.py b/tests/test_site_entry_tables.py index 4065bf8f6..ae4e01d0b 100644 --- a/tests/test_site_entry_tables.py +++ b/tests/test_site_entry_tables.py @@ -10,6 +10,8 @@ every name and hash back. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_site_exports.py b/tests/test_site_exports.py index 4d230afc9..22fb6a020 100644 --- a/tests/test_site_exports.py +++ b/tests/test_site_exports.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import json import os diff --git a/tests/test_site_funnel.py b/tests/test_site_funnel.py index 72a96fbf6..e6b113638 100644 --- a/tests/test_site_funnel.py +++ b/tests/test_site_funnel.py @@ -7,6 +7,8 @@ reader two levels deeper without saying so. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import os import re diff --git a/tests/test_site_images.py b/tests/test_site_images.py index 21d28c0f0..ee0cb5355 100644 --- a/tests/test_site_images.py +++ b/tests/test_site_images.py @@ -8,6 +8,8 @@ CDN. These tests hold the replacements in place. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_inputs.py b/tests/test_site_inputs.py index 57ee16fa4..031ae8b10 100644 --- a/tests/test_site_inputs.py +++ b/tests/test_site_inputs.py @@ -9,6 +9,8 @@ directory, so a get/ page said "No ZIP" for a setup the API marked as released. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import json import os diff --git a/tests/test_site_insights.py b/tests/test_site_insights.py index c9f9b2af9..ac0fe71c8 100644 --- a/tests/test_site_insights.py +++ b/tests/test_site_insights.py @@ -1,6 +1,8 @@ """The readings of scripts/site_insights.py, on synthetic figures.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import datetime as dt import sys import unittest diff --git a/tests/test_site_integration.py b/tests/test_site_integration.py index a3371662a..d2f58137c 100644 --- a/tests/test_site_integration.py +++ b/tests/test_site_integration.py @@ -9,6 +9,8 @@ Node's compile cache into /tmp. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import glob import json import re diff --git a/tests/test_site_nav.py b/tests/test_site_nav.py index af5731578..934c97fd4 100644 --- a/tests/test_site_nav.py +++ b/tests/test_site_nav.py @@ -21,6 +21,8 @@ are built, the pages and the site. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import struct diff --git a/tests/test_site_picker.py b/tests/test_site_picker.py index 7ff891551..d725f57df 100644 --- a/tests/test_site_picker.py +++ b/tests/test_site_picker.py @@ -9,6 +9,8 @@ list the page shows is not what the command it prints installs. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import importlib.util import json import random diff --git a/tests/test_site_platform_density.py b/tests/test_site_platform_density.py index d636261ba..92db586da 100644 --- a/tests/test_site_platform_density.py +++ b/tests/test_site_platform_density.py @@ -10,6 +10,8 @@ role of each reference named by one word that a legend defines once. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import html import re import sys diff --git a/tests/test_site_platform_hashes.py b/tests/test_site_platform_hashes.py index 7d131796b..bee30587a 100644 --- a/tests/test_site_platform_hashes.py +++ b/tests/test_site_platform_hashes.py @@ -8,6 +8,8 @@ checking a dump against the page was told the wrong hash. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from unittest import mock diff --git a/tests/test_site_privacy.py b/tests/test_site_privacy.py index 906d39ba7..9bf1e0808 100644 --- a/tests/test_site_privacy.py +++ b/tests/test_site_privacy.py @@ -6,6 +6,8 @@ page looking different. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from pathlib import Path diff --git a/tests/test_site_reference.py b/tests/test_site_reference.py index 36f130fde..e70976dd8 100644 --- a/tests/test_site_reference.py +++ b/tests/test_site_reference.py @@ -31,6 +31,8 @@ it has not been generated. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import csv import io import json diff --git a/tests/test_site_render.py b/tests/test_site_render.py index 9628bd5ed..09d25c7b1 100644 --- a/tests/test_site_render.py +++ b/tests/test_site_render.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_site_search.py b/tests/test_site_search.py index 976d8f860..242466820 100644 --- a/tests/test_site_search.py +++ b/tests/test_site_search.py @@ -19,6 +19,8 @@ built, the site. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import re import sys diff --git a/tests/test_site_system_links.py b/tests/test_site_system_links.py index 417ddd940..c9b132c01 100644 --- a/tests/test_site_system_links.py +++ b/tests/test_site_system_links.py @@ -11,6 +11,8 @@ Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_site_system_names.py b/tests/test_site_system_names.py index 2eeae53a4..170c12f9d 100644 --- a/tests/test_site_system_names.py +++ b/tests/test_site_system_names.py @@ -10,6 +10,8 @@ console folders, so the ids to name are the platforms' and the profiles'. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_site_system_sections.py b/tests/test_site_system_sections.py index 5029e4033..8be186cd0 100644 --- a/tests/test_site_system_sections.py +++ b/tests/test_site_system_sections.py @@ -14,6 +14,8 @@ Reads the repository: skips when the database is absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import sys diff --git a/tests/test_site_systems.py b/tests/test_site_systems.py index 9f8891799..726c6371b 100644 --- a/tests/test_site_systems.py +++ b/tests/test_site_systems.py @@ -30,6 +30,8 @@ three times before the first file. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import types diff --git a/tests/test_site_validation.py b/tests/test_site_validation.py index e7ec40fb4..b33ffbf7a 100644 --- a/tests/test_site_validation.py +++ b/tests/test_site_validation.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import re diff --git a/tests/test_site_weight.py b/tests/test_site_weight.py index 8918eba61..64ae626b0 100644 --- a/tests/test_site_weight.py +++ b/tests/test_site_weight.py @@ -9,6 +9,8 @@ is looked up by content, an emulator card's anchor sits on its row. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_site_wiki.py b/tests/test_site_wiki.py index cb0e17e7d..71efb0388 100644 --- a/tests/test_site_wiki.py +++ b/tests/test_site_wiki.py @@ -31,6 +31,8 @@ setup guides or the generator that writes the page a link opens. """ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import sys diff --git a/tests/test_site_write.py b/tests/test_site_write.py index d2c7342d2..e10709c1f 100644 --- a/tests/test_site_write.py +++ b/tests/test_site_write.py @@ -11,6 +11,8 @@ delete on the critical path and is why these tests exist. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import tempfile import unittest diff --git a/tests/test_slot.py b/tests/test_slot.py index 46dfd09be..65819a4ae 100644 --- a/tests/test_slot.py +++ b/tests/test_slot.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import unittest diff --git a/tests/test_slots.py b/tests/test_slots.py index 417f91f16..1c966fad3 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import re import shutil diff --git a/tests/test_slots_shipped_file.py b/tests/test_slots_shipped_file.py index c0865fb88..2b6dc03d3 100644 --- a/tests/test_slots_shipped_file.py +++ b/tests/test_slots_shipped_file.py @@ -11,6 +11,8 @@ Reads the repository: skips when the database or the files are absent. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import copy import hashlib import os diff --git a/tests/test_source_ref_targets.py b/tests/test_source_ref_targets.py index 611f0c739..5bf8eeee3 100644 --- a/tests/test_source_ref_targets.py +++ b/tests/test_source_ref_targets.py @@ -12,6 +12,8 @@ mirror pages its tree, and read as one page it could not confirm a path. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_split_extras.py b/tests/test_split_extras.py index 0f8c3c0ea..08690f001 100644 --- a/tests/test_split_extras.py +++ b/tests/test_split_extras.py @@ -7,6 +7,8 @@ and in no part (4507 of RetroArch's 5430). from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/tests/test_split_names.py b/tests/test_split_names.py index 250579cfb..98751f575 100644 --- a/tests/test_split_names.py +++ b/tests/test_split_names.py @@ -10,6 +10,8 @@ SHA256SUMS.txt. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_split_pack.py b/tests/test_split_pack.py index 2ae04dcde..48983f8fc 100644 --- a/tests/test_split_pack.py +++ b/tests/test_split_pack.py @@ -14,6 +14,8 @@ parts extracted into one folder are the pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_standalone_links.py b/tests/test_standalone_links.py index 9d8044fcc..7ea47c50f 100644 --- a/tests/test_standalone_links.py +++ b/tests/test_standalone_links.py @@ -12,6 +12,8 @@ naomi.zip took the place of MAME's. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import textwrap import unittest diff --git a/tests/test_standalone_mode.py b/tests/test_standalone_mode.py index 5e885c2c1..82d5d22b5 100644 --- a/tests/test_standalone_mode.py +++ b/tests/test_standalone_mode.py @@ -10,6 +10,8 @@ standalone_cores dropped them from its pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_standalone_probe.py b/tests/test_standalone_probe.py index 6a51f731c..c9eda2181 100644 --- a/tests/test_standalone_probe.py +++ b/tests/test_standalone_probe.py @@ -3,6 +3,8 @@ framebuffer reader, the settings it writes and the oracle it judges with.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import struct import sys import tempfile diff --git a/tests/test_system_ids.py b/tests/test_system_ids.py index 1b8098c58..48736036f 100644 --- a/tests/test_system_ids.py +++ b/tests/test_system_ids.py @@ -9,6 +9,8 @@ channelf.zip and SNES DSP sets to colecovision/. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_system_pack_extras.py b/tests/test_system_pack_extras.py index 29c23c8ff..069844ee0 100644 --- a/tests/test_system_pack_extras.py +++ b/tests/test_system_pack_extras.py @@ -7,6 +7,8 @@ names, while the split packs, built per system, carried them. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_system_pack_parity.py b/tests/test_system_pack_parity.py index 4cf8edc5f..9925a4310 100644 --- a/tests/test_system_pack_parity.py +++ b/tests/test_system_pack_parity.py @@ -10,6 +10,8 @@ or trees and credited a profile's file at a path holding another profile's. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import shutil diff --git a/tests/test_system_selection.py b/tests/test_system_selection.py index c7016c539..514c47262 100644 --- a/tests/test_system_selection.py +++ b/tests/test_system_selection.py @@ -10,6 +10,8 @@ libretro pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import re import subprocess diff --git a/tests/test_target_names.py b/tests/test_target_names.py index 877899c08..cb08e96b2 100644 --- a/tests/test_target_names.py +++ b/tests/test_target_names.py @@ -7,6 +7,8 @@ and `--verify-packs --target nx` looked for a pack nobody had built. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import tempfile diff --git a/tests/test_target_scrapers.py b/tests/test_target_scrapers.py index 6b05e356c..4bb1b7ba6 100644 --- a/tests/test_target_scrapers.py +++ b/tests/test_target_scrapers.py @@ -8,6 +8,8 @@ packs built for it shrank without an error. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import sys import unittest diff --git a/tests/test_target_systems.py b/tests/test_target_systems.py index c4896a123..ecb7adc6c 100644 --- a/tests/test_target_systems.py +++ b/tests/test_target_systems.py @@ -9,6 +9,8 @@ builder still shipped its ROMs as ep128emu extras. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_target_verify.py b/tests/test_target_verify.py index ee42f854b..586c9c7dc 100644 --- a/tests/test_target_verify.py +++ b/tests/test_target_verify.py @@ -10,6 +10,8 @@ out of a targeted --all run, and the check refuses it as the build does. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import contextlib diff --git a/tests/test_tool_messages.py b/tests/test_tool_messages.py index c2e59f6e9..7a7a61217 100644 --- a/tests/test_tool_messages.py +++ b/tests/test_tool_messages.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os @@ -85,6 +87,152 @@ class AMistypedTargetGetsTheClosestName(unittest.TestCase): +class TheSystemHintCountsTheListing(unittest.TestCase): + """`--system sony-playstaton` said "--list-systems lists all 579" and + --list-systems printed 532: the hint counted spellings, the listing + merges the ones that differ in case or separator.""" + + def test_both_tools_count_the_lines_the_listing_prints(self): + listing = subprocess.run( + [sys.executable, "scripts/verify.py", "--list-systems"], + cwd=REPO_ROOT, capture_output=True, text=True, timeout=300, check=True, + ) + printed = sum(1 for line in listing.stdout.splitlines() if "emulator" in line) + for script in ("verify.py", "generate_pack.py"): + with self.subTest(script=script), tempfile.TemporaryDirectory() as out: + argv = [sys.executable, f"scripts/{script}", "--system", "sony-playstaton"] + if script == "generate_pack.py": + argv += ["--output-dir", out] + proc = subprocess.run( + argv, cwd=REPO_ROOT, capture_output=True, text=True, timeout=300, + check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertIn(f"--list-systems lists all {printed}.", proc.stderr) + + +class ADamagedProfileNamesItself(unittest.TestCase): + """One misindented key in an unrelated profile stopped `verify.py + --emulator handy` and generate_pack on a 37-line yaml traceback, the + faulty file named near its end.""" + + def test_one_line_names_the_file_and_line(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + profiles = Path(folder) + (profiles / "handy.yml").write_text( + (REPO_ROOT / "emulators" / "handy.yml").read_text(encoding="utf-8"), + encoding="utf-8", + ) + (profiles / "bad.yml").write_text( + "emulator: Bad\nfiles:\n - name: a\n size: 3\n", encoding="utf-8" + ) + for script, extra in (("verify.py", []), + ("generate_pack.py", ["--output-dir", folder])): + with self.subTest(script=script): + proc = subprocess.run( + [sys.executable, f"scripts/{script}", "--emulator", "handy", + "--emulators-dir", folder, *extra], + cwd=REPO_ROOT, capture_output=True, text=True, timeout=300, + check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + self.assertEqual(proc.stderr.strip().splitlines(), [ + f"ERROR: {profiles / 'bad.yml'}:4: did not find expected '-' " + "indicator. Check it with: python scripts/validate_schemas.py " + "--source-only" + ]) + + def test_a_damaged_file_is_not_an_absent_one(self): + """Ten callers skip a platform whose file is absent (except + FileNotFoundError): a damaged one has to stop them instead.""" + self.assertFalse(issubclass(common.SourceUnreadable, FileNotFoundError)) + + +class TheVerdictClosesARedirectedReport(unittest.TestCase): + """`verify.py --emulator fbneo --strict > log 2>&1` put the --strict + ERROR on the first line of the log: stdout redirected is block-buffered, + stderr is not, and the report it closes came after.""" + + def test_stderr_stays_after_what_stdout_wrote_first(self): + program = ( + "import sys\n" + "sys.path.insert(0, 'scripts')\n" + "from common import run_cli\n" + "def main():\n" + " print('report')\n" + " print('ERROR: verdict', file=sys.stderr)\n" + " return 1\n" + "run_cli(main)\n" + ) + env = {k: v for k, v in os.environ.items() if k != "PYTHONUNBUFFERED"} + proc = subprocess.run( + [sys.executable, "-c", program], cwd=REPO_ROOT, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, timeout=60, check=False, env=env, + ) + self.assertEqual(proc.returncode, 1) + self.assertEqual(proc.stdout.splitlines(), ["report", "ERROR: verdict"]) + + +class TheAllHelpNamesWhatItCovers(unittest.TestCase): + """`verify.py --help` kept 'Verify all active platforms' after the + archived status was dropped: --all covers every registered platform, + and a reader looked for an --include-archived that no longer exists.""" + + def test_no_help_text_speaks_of_active_platforms(self): + proc = subprocess.run( + [sys.executable, "scripts/verify.py", "--help"], cwd=REPO_ROOT, + capture_output=True, text=True, timeout=60, check=True, + ) + self.assertIn("Verify every registered platform", proc.stdout) + stale = [path.name for path in sorted((REPO_ROOT / "scripts").rglob("*.py")) + if "active platform" in path.read_text(encoding="utf-8")] + self.assertEqual(stale, []) + + +class ReadOnlyToolsRunFromAnyFolder(unittest.TestCase): + """Only verify.py and generate_pack.py moved to the repository root. + From another folder slots, region_audit, provenance_report and + bundled_only said database.json was missing and to rebuild it, + identity and list_platforms named a missing folder, and game_rules + ended on a FileNotFoundError traceback.""" + + COMMANDS = ( + ["slots.py", "--platform", "retroarch"], + ["identity.py"], + ["cross_reference.py", "--emulator", "handy"], + ["list_platforms.py"], + ["region_audit.py"], + ["provenance_report.py"], + ["bundled_only.py"], + ["game_rules.py"], + ) + + def test_each_reads_the_repository_from_a_subfolder(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + for command in self.COMMANDS: + with self.subTest(command=command[0]): + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / command[0]), + *command[1:]], + cwd=folder, capture_output=True, text=True, timeout=300, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr[-500:]) + self.assertNotIn("Traceback", proc.stderr) + self.assertEqual(os.listdir(folder), []) + + def test_a_typed_path_is_read_from_where_it_was_typed(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "slots.py"), + "--db", "nosuch.json"], + cwd=folder, capture_output=True, text=True, timeout=300, check=False, + ) + self.assertEqual(proc.returncode, 1) + self.assertIn(str(Path(folder) / "nosuch.json"), proc.stderr) + + class AMistypedTargetAcrossAllPlatforms(unittest.TestCase): """`verify --all --target swich` ended on "no platform has it" after 17 INFO lines, two of which held the suggestion.""" diff --git a/tests/test_torrentzip.py b/tests/test_torrentzip.py index e8e3e61d5..b4986f629 100644 --- a/tests/test_torrentzip.py +++ b/tests/test_torrentzip.py @@ -1,6 +1,8 @@ """Tests for the TorrentZip builder against real MAME romsets.""" from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_truth_data_dir_readers.py b/tests/test_truth_data_dir_readers.py index f58978830..26818c367 100644 --- a/tests/test_truth_data_dir_readers.py +++ b/tests/test_truth_data_dir_readers.py @@ -9,6 +9,8 @@ revision, and hash_conflict did not see LRPS2's value. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_truth_diff.py b/tests/test_truth_diff.py index 949be7c7d..dcce0f7b3 100644 --- a/tests/test_truth_diff.py +++ b/tests/test_truth_diff.py @@ -9,6 +9,8 @@ discrepancy that does not exist. Content decides, as everywhere else here. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import pathlib import subprocess import sys diff --git a/tests/test_truth_layout.py b/tests/test_truth_layout.py index 50a23053b..d319b42f6 100644 --- a/tests/test_truth_layout.py +++ b/tests/test_truth_layout.py @@ -8,6 +8,8 @@ out on RetroBat, EmuDeck and RetroDECK. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import random diff --git a/tests/test_twin_declaration_status.py b/tests/test_twin_declaration_status.py index 4540adaaa..1cbb92c67 100644 --- a/tests/test_twin_declaration_status.py +++ b/tests/test_twin_declaration_status.py @@ -9,6 +9,8 @@ refusal read first left "excluded" behind the file the second one ships. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import hashlib import io diff --git a/tests/test_twin_declarations.py b/tests/test_twin_declarations.py index 4acbddf79..9ab464d93 100644 --- a/tests/test_twin_declarations.py +++ b/tests/test_twin_declarations.py @@ -9,6 +9,8 @@ frontend hashes bios/disk.rom for xroar and finds the PC-88 ROM. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_undeclared_dedup.py b/tests/test_undeclared_dedup.py index b7fa6cde5..9fc2c12bf 100644 --- a/tests/test_undeclared_dedup.py +++ b/tests/test_undeclared_dedup.py @@ -9,6 +9,8 @@ dropped from the pack. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_unplaced_extras.py b/tests/test_unplaced_extras.py index aab69f59d..cea234522 100644 --- a/tests/test_unplaced_extras.py +++ b/tests/test_unplaced_extras.py @@ -8,6 +8,8 @@ listed them neither in files nor in omitted_files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_upstream.py b/tests/test_upstream.py index 01817f52b..9132905be 100644 --- a/tests/test_upstream.py +++ b/tests/test_upstream.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import http.client import os diff --git a/tests/test_upstream_index.py b/tests/test_upstream_index.py index be389491d..a765dea57 100644 --- a/tests/test_upstream_index.py +++ b/tests/test_upstream_index.py @@ -8,6 +8,8 @@ and a RetroArch target pack lost LRPS2's files. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_validate_pr.py b/tests/test_validate_pr.py index d122ffc66..ddb73a493 100644 --- a/tests/test_validate_pr.py +++ b/tests/test_validate_pr.py @@ -8,6 +8,8 @@ it is an untrusted-input boundary with nothing behind it. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import os import sys import tempfile diff --git a/tests/test_validate_pr_flow.py b/tests/test_validate_pr_flow.py index cab20c4b8..d76259158 100644 --- a/tests/test_validate_pr_flow.py +++ b/tests/test_validate_pr_flow.py @@ -7,6 +7,8 @@ cannot hold has to be an action the contributor can take. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import os import subprocess diff --git a/tests/test_validate_pr_hashes.py b/tests/test_validate_pr_hashes.py index 280756681..110613039 100644 --- a/tests/test_validate_pr_hashes.py +++ b/tests/test_validate_pr_hashes.py @@ -8,6 +8,8 @@ were lost the same way. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import sys import unittest from pathlib import Path diff --git a/tests/test_validated_choice.py b/tests/test_validated_choice.py index f9aba6707..58df7e6a2 100644 --- a/tests/test_validated_choice.py +++ b/tests/test_validated_choice.py @@ -8,6 +8,8 @@ across 14 destinations on six platforms. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import hashlib import sys diff --git a/tests/test_validation_scope.py b/tests/test_validation_scope.py index 423f7b81b..31a313db6 100644 --- a/tests/test_validation_scope.py +++ b/tests/test_validation_scope.py @@ -8,6 +8,8 @@ test fails if either hands the index the whole profile set again. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import unittest from pathlib import Path diff --git a/tests/test_variant_choice.py b/tests/test_variant_choice.py index 2638e8657..48a187652 100644 --- a/tests/test_variant_choice.py +++ b/tests/test_variant_choice.py @@ -9,6 +9,8 @@ never swapped in. Both sides now read find_validated_variant. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import re diff --git a/tests/test_variant_identity.py b/tests/test_variant_identity.py index 1b1f99b46..1af95d7a1 100644 --- a/tests/test_variant_identity.py +++ b/tests/test_variant_identity.py @@ -9,6 +9,8 @@ database. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import re import unittest diff --git a/tests/test_verify_all_dir.py b/tests/test_verify_all_dir.py index 6e0077338..cc2b325ee 100644 --- a/tests/test_verify_all_dir.py +++ b/tests/test_verify_all_dir.py @@ -7,6 +7,8 @@ the run, and one registered only in the copy was never checked. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import json import subprocess import sys diff --git a/tests/test_verify_data_dir_notice.py b/tests/test_verify_data_dir_notice.py index b9e4a71f9..9d2b2d08d 100644 --- a/tests/test_verify_data_dir_notice.py +++ b/tests/test_verify_data_dir_notice.py @@ -9,6 +9,8 @@ verify --emulator already named its uncached trees. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import io import os diff --git a/tests/test_verify_emulator_evidence.py b/tests/test_verify_emulator_evidence.py index b8f498c37..9247d6caf 100644 --- a/tests/test_verify_emulator_evidence.py +++ b/tests/test_verify_emulator_evidence.py @@ -14,6 +14,8 @@ the other by flagging the divergence. This report agreed with neither. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_verify_listing.py b/tests/test_verify_listing.py index 00c43b592..5d6a208f9 100644 --- a/tests/test_verify_listing.py +++ b/tests/test_verify_listing.py @@ -2,6 +2,8 @@ from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import subprocess import sys import unittest @@ -241,6 +243,62 @@ class BundledGameDataIsNotMissing(unittest.TestCase): out.getvalue()) +class TheBundledLineSaysWhatItLeavesOut(unittest.TestCase): + """'Bundled with their emulator, not collected: 154' listed 14 names, + the required firmware, and nothing said 140 more existed.""" + + def test_the_hidden_names_are_counted(self): + import contextlib # noqa: PLC0415 + import io # noqa: PLC0415 + + sys.path.insert(0, str(REPO_ROOT / "scripts")) + import verify # noqa: PLC0415 + + result = {"undeclared_files": [ + {"name": name, "emulator": "e", "required": required, "in_repo": False, + "bundled": True, "in_pack": False} + for name, required in (("fw.bin", True), ("a.frag", False), ("b.dll", False)) + ]} + with contextlib.redirect_stdout(io.StringIO()) as quiet: + verify._print_undeclared_section(result, verbose=False) + self.assertIn( + "Bundled with their emulator, not collected: 3 " + "(fw.bin, 2 more, --verbose lists them)", + quiet.getvalue(), + ) + with contextlib.redirect_stdout(io.StringIO()) as verbose: + verify._print_undeclared_section(result, verbose=True) + self.assertIn( + "Bundled with their emulator, not collected: 3 (a.frag, b.dll, fw.bin)", + verbose.getvalue(), + ) + result["undeclared_files"] = result["undeclared_files"][1:] + with contextlib.redirect_stdout(io.StringIO()) as unnamed: + verify._print_undeclared_section(result, verbose=False) + self.assertIn("Bundled with their emulator, not collected: 2 (--verbose lists them)", + unnamed.getvalue()) + + +class TheCheckLineSaysWhatItIs(unittest.TestCase): + """Without --verbose each missing core file was followed by a bare + '[size]' line, nine of them in verify --all.""" + + def test_the_checks_read_as_the_verbose_line(self): + import contextlib # noqa: PLC0415 + import io # noqa: PLC0415 + + sys.path.insert(0, str(REPO_ROOT / "scripts")) + import verify # noqa: PLC0415 + + entry = {"name": "FBASIC30.ROM", "emulator": "XM7", "checks": ["size"]} + with contextlib.redirect_stdout(io.StringIO()) as out: + verify._print_undeclared_entry(entry, "MISSING (required)", False, set()) + self.assertEqual(out.getvalue().splitlines(), [ + " MISSING (required): XM7 needs FBASIC30.ROM", + " validates size", + ]) + + class CoreCopiesWithoutAGapEntryAreCounted(unittest.TestCase): """verify --platform recalbox --system nec-pc-engine printed no Core files line while the pack held six geargrafx syscard copies: the gap diff --git a/tests/test_verify_pack_choice.py b/tests/test_verify_pack_choice.py index 8da4070a7..96c6e2d22 100644 --- a/tests/test_verify_pack_choice.py +++ b/tests/test_verify_pack_choice.py @@ -8,6 +8,8 @@ regional one. A pack built with --source truth answered SKIP and exit 0. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import argparse import json import os diff --git a/tests/test_verify_release_asset.py b/tests/test_verify_release_asset.py index 161732f53..d4cc4ac16 100644 --- a/tests/test_verify_release_asset.py +++ b/tests/test_verify_release_asset.py @@ -11,6 +11,8 @@ counts the file missing as before. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import os import sys diff --git a/tests/test_version_basis.py b/tests/test_version_basis.py index bb0f01511..55e1bb9af 100644 --- a/tests/test_version_basis.py +++ b/tests/test_version_basis.py @@ -8,6 +8,8 @@ to a basis, and hold a pin advance to reading the version again. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import contextlib import copy import hashlib diff --git a/tests/test_which_pack.py b/tests/test_which_pack.py index 0323b4dcb..6fd282a40 100644 --- a/tests/test_which_pack.py +++ b/tests/test_which_pack.py @@ -6,6 +6,8 @@ published and carries 3916 files the RetroArch one does not. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import re import sys import unittest diff --git a/tests/test_workflow_paths.py b/tests/test_workflow_paths.py index 119013586..1181abb82 100644 --- a/tests/test_workflow_paths.py +++ b/tests/test_workflow_paths.py @@ -7,6 +7,8 @@ until some unrelated file moved. from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import ast import fnmatch import re diff --git a/tests/test_zipped_member.py b/tests/test_zipped_member.py index da0d63eee..cc369fbd0 100644 --- a/tests/test_zipped_member.py +++ b/tests/test_zipped_member.py @@ -6,6 +6,8 @@ level1.rom went out as OK where verify said "not found inside ZIP". from __future__ import annotations +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + import hashlib import sys import tempfile diff --git a/wiki/reading-reports.md b/wiki/reading-reports.md index a3bfd6765..844d03a78 100644 --- a/wiki/reading-reports.md +++ b/wiki/reading-reports.md @@ -110,7 +110,7 @@ RetroDECK: 2007/2008 OK, 1 untested [md5] Core files: 1764 in pack, 1 required missing, 26 optional missing (--verbose lists them) Core files held but not packed: 154 (142 installed with its emulator, 8 read below a save folder per system and no system runs the core there, 4 a packed file occupies its directory) MISSING (required): FinalBurn Neo needs sfiii2a.zip (1 file) - Bundled with their emulator, not collected: 20 + Bundled with their emulator, not collected: 20 (--verbose lists them) Game data: 5099 in pack, 30 missing (--verbose lists them) Game data held but not packed: 30 (installed with its emulator) No external files: 78 cores (--verbose lists them) diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 3345889d1..9e32c6481 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -222,8 +222,14 @@ disambiguated by a hash suffix on purpose. output or formatting. 5. **Run the full suite.** After adding your test, run `python -m unittest - discover -s tests -t . -v` to verify nothing else broke. `-t .` imports - `tests/__init__.py`, which keeps the suite offline. + discover -s tests -t . -v` to verify nothing else broke. + +A new module starts with `import tests` right after +`from __future__ import annotations`. The package import is what keeps a run +offline, puts its scratch under `tmp/` and gives it its own installer cache, +however the module is started: `discover tests`, `python -m unittest +tests.test_e2e` or `python -m tests.test_e2e`. A test fails on any module +without it. A fix comes with a test that fails without it. Write the test first, watch it fail on the current code, then fix: a test written after the fix may never @@ -488,6 +494,19 @@ wired to pull requests alone would guard the road nobody takes. The test job with BIOS validation and auto-labeling too. Those two read pull request context and stay behind an event guard. +The schema job also runs mypy over `install.py` and `scripts/`. The code +carried 993 type errors when the check started, so they sit in +`mypy-baseline.txt` and only a new one fails. An error the change removes fails +too until the baseline is synced, so the file never hides a later error that +reads the same: + +```bash +pip install -e ".[dev]" +mypy > tmp/mypy.txt +mypy-baseline filter < tmp/mypy.txt # what CI runs +mypy-baseline sync < tmp/mypy.txt # after a fix, commit the new baseline +``` + A push series collapses to the tip, so what a green run states is that the head of main passes, not every commit under it.