From 18fd8a8a49a731e9048b05ace641988604890811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Bonhomme?= Date: Sun, 22 Jun 2025 12:03:30 +0200 Subject: [PATCH] chg: [typing] Make Mypy Happy Again. --- .pre-commit-config.yaml | 2 +- pyproject.toml | 9 +++++++-- stegano/red/red.py | 14 +++++++++++--- stegano/steganalysis/parity.py | 4 +++- stegano/tools.py | 35 ++++++++++++++++++++-------------- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7720ecb..e814e8f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: additional_dependencies: - flake8-bugbear - flake8-implicit-str-concat - args: ["--max-line-length=125"] + args: ["--max-line-length=125", "--ignore=E203"] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index 3d3db08..8c0101f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ optional = true [tool.mypy] -python_version = "3.12" +python_version = "3.13" check_untyped_defs = true ignore_errors = false ignore_missing_imports = true @@ -87,7 +87,12 @@ warn_unreachable = true show_error_context = true pretty = true -exclude = "build|dist|docs|stegano.egg-info" +exclude = "build|dist|docs" + [tool.isort] profile = "black" + + +[tool.flake8] +ignore = ["E203"] diff --git a/stegano/red/red.py b/stegano/red/red.py index 1c7a7ef..e6e2429 100755 --- a/stegano/red/red.py +++ b/stegano/red/red.py @@ -23,7 +23,7 @@ __date__ = "$Date: 2010/10/01 $" __revision__ = "$Date: 2017/02/06 $" __license__ = "GPLv3" -from typing import IO, Union +from typing import IO, Union, cast from stegano import tools @@ -40,13 +40,17 @@ def hide(input_image: Union[str, IO[bytes]], message: str): assert message_length != 0, "message message_length is zero" assert message_length < 255, "message is too long" img = tools.open_image(input_image) + # Ensure image mode is RGB + if img.mode != "RGB": + img = img.convert("RGB") # Use a copy of image to hide the text in encoded = img.copy() width, height = img.size index = 0 for row in range(height): for col in range(width): - (r, g, b) = img.getpixel((col, row)) + pixel = cast(tuple[int, int, int], img.getpixel((col, row))) + r, g, b = pixel # first value is message_length of message if row == 0 and col == 0 and index < message_length: asc = message_length @@ -70,12 +74,16 @@ def reveal(input_image: Union[str, IO[bytes]]): The red value of the first pixel is used for message_length of string. """ img = tools.open_image(input_image) + # Ensure image mode is RGB + if img.mode != "RGB": + img = img.convert("RGB") width, height = img.size message = "" index = 0 for row in range(height): for col in range(width): - r, g, b = img.getpixel((col, row)) + pixel = cast(tuple[int, int, int], img.getpixel((col, row))) + r, g, b = pixel # First pixel r value is length of message if row == 0 and col == 0: message_length = r diff --git a/stegano/steganalysis/parity.py b/stegano/steganalysis/parity.py index deb185f..36cb42a 100644 --- a/stegano/steganalysis/parity.py +++ b/stegano/steganalysis/parity.py @@ -23,6 +23,8 @@ __date__ = "$Date: 2010/10/01 $" __revision__ = "$Date: 2019/06/06 $" __license__ = "GPLv3" +from typing import cast + from PIL import Image @@ -34,7 +36,7 @@ def steganalyse(img: Image.Image) -> Image.Image: width, height = img.size for row in range(height): for col in range(width): - if pixel := img.getpixel((col, row)): + if pixel := cast(tuple[int, int, int], img.getpixel((col, row))): r, g, b = pixel[0:3] else: raise Exception("Error during steganlysis.") diff --git a/stegano/tools.py b/stegano/tools.py index f4bcab7..fc018f7 100755 --- a/stegano/tools.py +++ b/stegano/tools.py @@ -26,7 +26,7 @@ __license__ = "GPLv3" import base64 import itertools from functools import reduce -from typing import IO, List, Union +from typing import IO, List, Union, cast from PIL import Image @@ -101,11 +101,12 @@ def base642binary(b64_fname: str) -> bytes: return base64.b64decode(b64_fname) -def open_image(fname_or_instance: Union[str, IO[bytes]]): - """Opens a Image and returns it. +def open_image(fname_or_instance: Union[str, IO[bytes], Image.Image]) -> Image.Image: + """Opens an image and returns it. - :param fname_or_instance: Can either be the location of the image as a - string or the Image.Image instance itself. + :param fname_or_instance: Can be a path to the image (str), + a file-like object (IO[bytes]), + or a PIL Image instance. """ if isinstance(fname_or_instance, Image.Image): return fname_or_instance @@ -157,8 +158,15 @@ class Hider: return True if self._index + 3 <= self._len_message_bits else False def encode_pixel(self, coordinate: tuple): - # Get the colour component. - r, g, b, *a = self.encoded_image.getpixel(coordinate) + # Determine expected pixel format based on mode + if self.encoded_image.mode == "RGBA": + r, g, b, *a = cast( + tuple[int, int, int, int], self.encoded_image.getpixel(coordinate) + ) + else: + r, g, b, *a = cast( + tuple[int, int, int], self.encoded_image.getpixel(coordinate) + ) # Change the Least Significant Bit of each colour component. r = setlsb(r, self._message_bits[self._index]) @@ -190,8 +198,11 @@ class Revealer: self.close_file = close_file def decode_pixel(self, coordinate: tuple): - # pixel = [r, g, b] or [r,g,b,a] - pixel = self.encoded_image.getpixel(coordinate) + # Tell mypy that this will be a 3- or 4-tuple of ints + pixel = cast( + tuple[int, int, int] | tuple[int, int, int, int], + self.encoded_image.getpixel(coordinate), + ) if self.encoded_image.mode == "RGBA": pixel = pixel[:3] # ignore the alpha @@ -211,13 +222,9 @@ class Revealer: raise IndexError("Impossible to detect message.") if len(self._bitab) - len(str(self._limit)) - 1 == self._limit: - self.secret_message = "".join(self._bitab)[ - len(str(self._limit)) + 1 : # noqa: E203 - ] + self.secret_message = "".join(self._bitab)[len(str(self._limit)) + 1 :] if self.close_file: self.encoded_image.close() - return True - else: return False