From 3773f47c9c2b4bb0c5a7c3716ce67f3b97d26d9c Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 21 Sep 2024 20:43:32 -0400 Subject: [PATCH] Update minimal Python version to 3.11: (#296) - Switch from "toml" to "tomlib + tomli-w" - Fixes an issue reading mixed types lists https://github.com/uiri/toml/issues/270 (and empty sets writing `[false,]` but that has no open issues) - Use StrEnum to simplify `CaptureMethodEnum` --- .github/workflows/lint-and-build.yml | 4 +-- .sonarcloud.properties | 1 - README.md | 4 ++- docs/build instructions.md | 2 +- pyproject.toml | 2 +- ruff.toml | 2 +- scripts/python_build_from_source_linux.bash | 12 +++---- scripts/requirements.txt | 2 +- src/AutoSplitImage.py | 6 ++-- src/capture_method/__init__.py | 37 +++++---------------- src/menu_bar.py | 6 ++-- src/user_profile.py | 20 ++++++----- 12 files changed, 41 insertions(+), 57 deletions(-) delete mode 100644 .sonarcloud.properties diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml index 9b1b9353..af64be9c 100644 --- a/.github/workflows/lint-and-build.yml +++ b/.github/workflows/lint-and-build.yml @@ -46,7 +46,7 @@ jobs: # Ruff is version and platform sensible matrix: os: [windows-latest, ubuntu-22.04] - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v4 @@ -66,7 +66,7 @@ jobs: # Pyright is version and platform sensible matrix: os: [windows-latest, ubuntu-22.04] - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v4 diff --git a/.sonarcloud.properties b/.sonarcloud.properties deleted file mode 100644 index 56192197..00000000 --- a/.sonarcloud.properties +++ /dev/null @@ -1 +0,0 @@ -sonar.python.version=3.10, 3.11, 3.12 diff --git a/README.md b/README.md index d8de43e0..3ef06900 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ To understand how to use AutoSplit and how it works in-depth, please read the [t - Should work on Ubuntu 20.04+ (Only tested on Ubuntu 22.04) - Wayland is not currently supported - WSL2/WSLg requires an additional Desktop Environment, external X11 server, and/or systemd -- Python 3.10+ (Not required for normal use. Refer to the [build instructions](/docs/build%20instructions.md) if you'd like run the application directly in Python). +- Python 3.11+ (Not required for normal use. Refer to the [build instructions](/docs/build%20instructions.md) if you'd like run the application directly in Python). ## Timer Integration @@ -102,6 +102,8 @@ Not a developer? You can still help through the following methods: - - - + - + - - - - diff --git a/docs/build instructions.md b/docs/build instructions.md index 55f22a76..bbf78cb5 100644 --- a/docs/build instructions.md +++ b/docs/build instructions.md @@ -13,7 +13,7 @@ ### All platforms -- [Python](https://www.python.org/downloads/) 3.10+. +- [Python](https://www.python.org/downloads/) 3.11+. - [Node](https://nodejs.org) is optional, but required for complete linting. - Alternatively you can install the [pyright python wrapper](https://pypi.org/project/pyright/) which has a bit of an overhead delay. - [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell) is used to run all the scripts diff --git a/pyproject.toml b/pyproject.toml index 51958d6f..ef243b38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file [tool.pyright] typeCheckingMode = "strict" -pythonVersion = "3.10" +pythonVersion = "3.11" # Prefer `pyright: ignore` enableTypeIgnoreComments = false diff --git a/ruff.toml b/ruff.toml index 79adc33c..d6536ecd 100644 --- a/ruff.toml +++ b/ruff.toml @@ -7,7 +7,7 @@ # These configs are incompatible with ruff<0.5.7 # https://docs.astral.sh/ruff/configuration/ -target-version = "py310" # Change this to the oldest supported version by your application +target-version = "py311" # Change this to the oldest supported version by your application line-length = 100 preview = true diff --git a/scripts/python_build_from_source_linux.bash b/scripts/python_build_from_source_linux.bash index 20c82e75..7e9fb839 100644 --- a/scripts/python_build_from_source_linux.bash +++ b/scripts/python_build_from_source_linux.bash @@ -7,22 +7,22 @@ sudo apt update sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev tk-dev # Download Python binary package: -wget https://www.python.org/ftp/python/3.10.13/Python-3.10.13.tgz +wget https://www.python.org/ftp/python/3.11.10/Python-3.11.10.tgz # Unzip the package: -tar -xzf Python-3.10.13.tgz +tar -xzf Python-3.11.10.tgz # Execute configure script -cd Python-3.10.13 +cd Python-3.11.10 ./configure --enable-optimizations --enable-shared -# Build Python 3.10 +# Build Python 3.11 make -j 2 -# Install Python 3.10 +# Install Python 3.11 sudo make install # Verify the installation -python3.10 -V +python3.11 -V echo "If Python version did not print, you may need to stop active processes" diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 95a1a2bd..1e9ba49a 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -15,7 +15,7 @@ PyWinCtl>=0.0.42 # py.typed # When needed, dev builds can be found at https://download.qt.io/snapshots/ci/pyside/dev?C=M;O=D PySide6-Essentials>=6.6.0 # Python 3.12 support scipy>=1.11.2 # Python 3.12 support -toml +tomli-w typing-extensions>=4.4.0 # @override decorator support # # Build and compile resources diff --git a/src/AutoSplitImage.py b/src/AutoSplitImage.py index b2b5e26b..3e69a27b 100644 --- a/src/AutoSplitImage.py +++ b/src/AutoSplitImage.py @@ -1,11 +1,11 @@ import os +import tomllib from enum import IntEnum, auto from math import sqrt from typing import TYPE_CHECKING import cv2 import numpy as np -import toml from cv2.typing import MatLike import error_messages @@ -133,8 +133,8 @@ def __parse_text_file(self, path: str): error_messages.tesseract_missing(path) return - with open(path, encoding="utf-8") as f: - data = toml.load(f) + with open(path, mode="rb") as f: + data = tomllib.load(f) self.texts = [text.lower().strip() for text in data["texts"]] self.__rect = (data["left"], data["right"], data["top"], data["bottom"]) diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index e6496c7e..6d25cda1 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -2,11 +2,11 @@ import sys from collections import OrderedDict from dataclasses import dataclass -from enum import Enum, EnumMeta, auto, unique +from enum import EnumMeta, StrEnum, auto, unique from itertools import starmap -from typing import TYPE_CHECKING, TypedDict, cast +from typing import TYPE_CHECKING, Never, TypedDict, cast -from typing_extensions import Never, override +from typing_extensions import override from capture_method.CaptureMethodBase import CaptureMethodBase from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod @@ -49,7 +49,7 @@ class Region(TypedDict): height: int -class CaptureMethodEnumMeta(EnumMeta): +class ContainerEnumMeta(EnumMeta): # Allow checking if simple string is enum @override def __contains__(cls, other: object): @@ -61,35 +61,16 @@ def __contains__(cls, other: object): @unique -# TODO: Try StrEnum in Python 3.11 -class CaptureMethodEnum(Enum, metaclass=CaptureMethodEnumMeta): - # Allow TOML to save as a simple string - @override - def __repr__(self): - return self.value - - # Allow direct comparison with strings - @override - def __eq__(self, other: object): - if isinstance(other, str): - return self.value == other - if isinstance(other, Enum): - return self.value == other.value - return other == self - - # Restore hashing functionality for use in Maps - @override - def __hash__(self): - return self.value.__hash__() - +class CaptureMethodEnum(StrEnum, metaclass=ContainerEnumMeta): + # Capitalize the string value from auto() @override @staticmethod def _generate_next_value_( - name: "str | CaptureMethodEnum", + name: str, start: int, count: int, - last_values: list["str | CaptureMethodEnum"], - ): + last_values: list[str], + ) -> str: return name NONE = "" diff --git a/src/menu_bar.py b/src/menu_bar.py index a655735a..0b9246f3 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -362,7 +362,7 @@ def __setup_bindings(self): def add_or_del(checked: Literal[0, 2], command: CommandStr = command): if checked: - _screenshot_on_setting.add(command) + _screenshot_on_setting.append(command) else: _screenshot_on_setting.remove(command) @@ -502,10 +502,10 @@ def get_default_settings_from_ui(autosplit: "AutoSplit"): "split_image_directory": autosplit.split_image_folder_input.text(), "screenshot_directory": default_settings_dialog.screenshot_directory_input.text(), "open_screenshot": default_settings_dialog.open_screenshot_checkbox.isChecked(), - "screenshot_on": { + "screenshot_on": [ getattr(default_settings_dialog, f"screenshot_on_{command}_checkbox").isChecked() for command in _DEBUG_SCREENSHOT_COMMANDS - }, + ], "captured_window_title": "", "capture_region": { "x": autosplit.x_spinbox.value(), diff --git a/src/user_profile.py b/src/user_profile.py index fdf8d193..ca8f0ecd 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -1,8 +1,9 @@ import os +import tomllib from copy import deepcopy from typing import TYPE_CHECKING, TypedDict, cast -import toml +import tomli_w from PySide6 import QtCore, QtWidgets from typing_extensions import deprecated, override @@ -40,7 +41,7 @@ class UserProfileDict(TypedDict): split_image_directory: str screenshot_directory: str open_screenshot: bool - screenshot_on: set[CommandStr] + screenshot_on: list[CommandStr] captured_window_title: str capture_region: Region @@ -73,7 +74,7 @@ def copy(): split_image_directory="", screenshot_directory="", open_screenshot=True, - screenshot_on=set(), + screenshot_on=[], captured_window_title="", capture_region=Region(x=0, y=0, width=1, height=1), ) @@ -112,8 +113,9 @@ def save_settings_as(autosplit: "AutoSplit"): def __save_settings_to_file(autosplit: "AutoSplit", save_settings_file_path: str): # Save settings to a .toml file - with open(save_settings_file_path, "w", encoding="utf-8") as file: - toml.dump(autosplit.settings_dict, file) + with open(save_settings_file_path, "wb") as file: + # https://github.com/hukkin/tomli-w/pull/46 + tomli_w.dump(autosplit.settings_dict, file) # pyright: ignore[reportArgumentType] autosplit.last_saved_settings = deepcopy(autosplit.settings_dict) autosplit.last_successfully_loaded_settings_file_path = save_settings_file_path return save_settings_file_path @@ -132,14 +134,14 @@ def __load_settings_from_file(autosplit: "AutoSplit", load_settings_file_path: s settings_widget.close() try: - with open(load_settings_file_path, encoding="utf-8") as file: + with open(load_settings_file_path, mode="rb") as file: # Casting here just so we can build an actual UserProfileDict once we're done validating # Fallback to default settings if some are missing from the file. # This happens when new settings are added. - loaded_settings = DEFAULT_PROFILE | cast(UserProfileDict, toml.load(file)) + loaded_settings = DEFAULT_PROFILE | cast(UserProfileDict, tomllib.load(file)) # TODO: Data Validation / fallbacks ? - loaded_settings["screenshot_on"] = set(loaded_settings["screenshot_on"]) + loaded_settings["screenshot_on"] = list(set(loaded_settings["screenshot_on"])) autosplit.settings_dict = UserProfileDict(**loaded_settings) autosplit.last_saved_settings = deepcopy(autosplit.settings_dict) @@ -148,7 +150,7 @@ def __load_settings_from_file(autosplit: "AutoSplit", load_settings_file_path: s autosplit.width_spinbox.setValue(autosplit.settings_dict["capture_region"]["width"]) autosplit.height_spinbox.setValue(autosplit.settings_dict["capture_region"]["height"]) autosplit.split_image_folder_input.setText(autosplit.settings_dict["split_image_directory"]) - except (FileNotFoundError, MemoryError, TypeError, toml.TomlDecodeError): + except (FileNotFoundError, MemoryError, TypeError, tomllib.TOMLDecodeError): autosplit.show_error_signal.emit(error_messages.invalid_settings) return False