Skip to content

Commit

Permalink
Add type hints to Tests/helper.py
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk committed Jan 19, 2024
1 parent 6dc6d6d commit 90babc8
Showing 1 changed file with 71 additions and 38 deletions.
109 changes: 71 additions & 38 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import sysconfig
import tempfile
from io import BytesIO
from typing import Sequence, Any, Callable

import pytest
from packaging.version import parse as parse_version
Expand All @@ -28,16 +29,16 @@

class test_image_results:
@staticmethod
def upload(a, b):
def upload(a: Image.Image, b: Image.Image) -> None:
a.show()
b.show()

elif "GITHUB_ACTIONS" in os.environ:
HAS_UPLOADER = True

class test_image_results:
class test_image_results: # type: ignore[no-redef]
@staticmethod
def upload(a, b):
def upload(a: Image.Image, b: Image.Image) -> str:
dir_errors = os.path.join(os.path.dirname(__file__), "errors")
os.makedirs(dir_errors, exist_ok=True)
tmpdir = tempfile.mkdtemp(dir=dir_errors)
Expand All @@ -47,14 +48,16 @@ def upload(a, b):

else:
try:
import test_image_results
import test_image_results # type: ignore[import-untyped, no-redef]

HAS_UPLOADER = True
except ImportError:
pass


def convert_to_comparable(a, b):
def convert_to_comparable(
a: Image.Image, b: Image.Image
) -> tuple[Image.Image, Image.Image]:
new_a, new_b = a, b
if a.mode == "P":
new_a = Image.new("L", a.size)
Expand All @@ -67,14 +70,16 @@ def convert_to_comparable(a, b):
return new_a, new_b


def assert_deep_equal(a, b, msg=None):
def assert_deep_equal(a: Sequence, b: Sequence, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
assert a == b, msg


def assert_image(im, mode, size, msg=None):
def assert_image(
im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None
) -> None:
if mode is not None:
assert im.mode == mode, (
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
Expand All @@ -86,7 +91,7 @@ def assert_image(im, mode, size, msg=None):
)


def assert_image_equal(a, b, msg=None):
def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
if a.tobytes() != b.tobytes():
Expand All @@ -100,14 +105,18 @@ def assert_image_equal(a, b, msg=None):
pytest.fail(msg or "got different content")


def assert_image_equal_tofile(a, filename, msg=None, mode=None):
def assert_image_equal_tofile(
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_equal(a, img, msg)


def assert_image_similar(a, b, epsilon, msg=None):
def assert_image_similar(
a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None
) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"

Expand All @@ -134,24 +143,32 @@ def assert_image_similar(a, b, epsilon, msg=None):
raise e


def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None):
def assert_image_similar_tofile(
a: Image.Image,
filename: str,
epsilon: float,
msg: str | None = None,
mode: str | None = None,
) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg)


def assert_all_same(items, msg=None):
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg


def assert_not_all_same(items, msg=None):
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg


def assert_tuple_approx_equal(actuals, targets, threshold, msg):
def assert_tuple_approx_equal(
actuals: tuple[int, ...], targets: tuple[int, ...], threshold: int, msg: str
) -> None:
"""Tests if actuals has values within threshold from targets"""
value = True
value = 1
for i, target in enumerate(targets):
value *= target - threshold <= actuals[i] <= target + threshold

Expand All @@ -163,17 +180,24 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
return pytest.mark.skipif(not features.check(feature), reason=reason)


def skip_unless_feature_version(feature, version_required, reason=None):
def skip_unless_feature_version(
feature: str, version_required: str, reason: str | None = None
) -> pytest.MarkDecorator:
if not features.check(feature):
return pytest.mark.skip(f"{feature} not available")
if reason is None:
reason = f"{feature} is older than {version_required}"
version_required = parse_version(version_required)
version_required_ = parse_version(version_required)
version_available = parse_version(features.version(feature))
return pytest.mark.skipif(version_available < version_required, reason=reason)
return pytest.mark.skipif(version_available < version_required_, reason=reason)


def mark_if_feature_version(mark, feature, version_blacklist, reason=None):
def mark_if_feature_version(
mark: pytest.MarkDecorator,
feature: str,
version_blacklist: str,
reason: str | None = None,
) -> pytest.MarkDecorator:
if not features.check(feature):
return pytest.mark.pil_noop_mark()
if reason is None:
Expand All @@ -194,7 +218,7 @@ class PillowLeakTestCase:
iterations = 100 # count
mem_limit = 512 # k

def _get_mem_usage(self):
def _get_mem_usage(self) -> float:
"""
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
between macOS and Linux rss reporting
Expand All @@ -216,7 +240,7 @@ def _get_mem_usage(self):
# This is the maximum resident set size used (in kilobytes).
return mem # Kb

def _test_leak(self, core):
def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage()
for cycle in range(self.iterations):
core()
Expand All @@ -228,17 +252,22 @@ def _test_leak(self, core):
# helpers


def fromstring(data):
def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data))


def tostring(im, string_format, **options):
def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
out = BytesIO()
im.save(out, string_format, **options)
return out.getvalue()


def hopper(mode=None, cache={}):
def hopper(
mode: str | None = None, cache: dict[str, Image.Image] | None = None
) -> Image.Image:
if cache is None:
cache = {}

if mode is None:
# Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors
Expand All @@ -259,29 +288,31 @@ def hopper(mode=None, cache={}):
return im.copy()


def djpeg_available():
def djpeg_available() -> bool:
if shutil.which("djpeg"):
try:
subprocess.check_call(["djpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
return False


def cjpeg_available():
def cjpeg_available() -> bool:
if shutil.which("cjpeg"):
try:
subprocess.check_call(["cjpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
return False


def netpbm_available():
def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))


def magick_command():
def magick_command() -> list[str] | None:
if sys.platform == "win32":
magickhome = os.environ.get("MAGICK_HOME")
if magickhome:
Expand All @@ -299,46 +330,48 @@ def magick_command():
if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick

return None


def on_appveyor():
def on_appveyor() -> bool:
return "APPVEYOR" in os.environ


def on_github_actions():
def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ


def on_ci():
def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ


def is_big_endian():
def is_big_endian() -> bool:
return sys.byteorder == "big"


def is_ppc64le():
def is_ppc64le() -> bool:
import platform

return platform.machine() == "ppc64le"


def is_win32():
def is_win32() -> bool:
return sys.platform.startswith("win32")


def is_pypy():
def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info")


def is_mingw():
def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw"


class CachedProperty:
def __init__(self, func):
def __init__(self, func: Callable[[Any], Any]) -> None:
self.func = func

def __get__(self, instance, cls=None):
def __get__(self, instance: dict[Any, Callable[[], Any]], cls: Any = None) -> Any:
result = instance.__dict__[self.func.__name__] = self.func(instance)
return result

0 comments on commit 90babc8

Please sign in to comment.