Skip to content

Commit 6fe683b

Browse files
authored
Merge pull request #7733 from radarhere/type_hints_helper
Added type hints to Tests/helper.py
2 parents 4aa3341 + a18cee3 commit 6fe683b

File tree

1 file changed

+95
-72
lines changed

1 file changed

+95
-72
lines changed

Tests/helper.py

Lines changed: 95 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import sysconfig
1212
import tempfile
1313
from io import BytesIO
14+
from typing import Any, Callable, Sequence
1415

1516
import pytest
1617
from packaging.version import parse as parse_version
@@ -19,42 +20,40 @@
1920

2021
logger = logging.getLogger(__name__)
2122

22-
23-
HAS_UPLOADER = False
24-
23+
uploader = None
2524
if os.environ.get("SHOW_ERRORS"):
26-
# local img.show for errors.
27-
HAS_UPLOADER = True
28-
29-
class test_image_results:
30-
@staticmethod
31-
def upload(a, b):
32-
a.show()
33-
b.show()
34-
25+
uploader = "show"
3526
elif "GITHUB_ACTIONS" in os.environ:
36-
HAS_UPLOADER = True
37-
38-
class test_image_results:
39-
@staticmethod
40-
def upload(a, b):
41-
dir_errors = os.path.join(os.path.dirname(__file__), "errors")
42-
os.makedirs(dir_errors, exist_ok=True)
43-
tmpdir = tempfile.mkdtemp(dir=dir_errors)
44-
a.save(os.path.join(tmpdir, "a.png"))
45-
b.save(os.path.join(tmpdir, "b.png"))
46-
return tmpdir
47-
27+
uploader = "github_actions"
4828
else:
4929
try:
5030
import test_image_results
5131

52-
HAS_UPLOADER = True
32+
uploader = "aws"
5333
except ImportError:
5434
pass
5535

5636

57-
def convert_to_comparable(a, b):
37+
def upload(a: Image.Image, b: Image.Image) -> str | None:
38+
if uploader == "show":
39+
# local img.show for errors.
40+
a.show()
41+
b.show()
42+
elif uploader == "github_actions":
43+
dir_errors = os.path.join(os.path.dirname(__file__), "errors")
44+
os.makedirs(dir_errors, exist_ok=True)
45+
tmpdir = tempfile.mkdtemp(dir=dir_errors)
46+
a.save(os.path.join(tmpdir, "a.png"))
47+
b.save(os.path.join(tmpdir, "b.png"))
48+
return tmpdir
49+
elif uploader == "aws":
50+
return test_image_results.upload(a, b)
51+
return None
52+
53+
54+
def convert_to_comparable(
55+
a: Image.Image, b: Image.Image
56+
) -> tuple[Image.Image, Image.Image]:
5857
new_a, new_b = a, b
5958
if a.mode == "P":
6059
new_a = Image.new("L", a.size)
@@ -67,14 +66,18 @@ def convert_to_comparable(a, b):
6766
return new_a, new_b
6867

6968

70-
def assert_deep_equal(a, b, msg=None):
69+
def assert_deep_equal(
70+
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
71+
) -> None:
7172
try:
7273
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
7374
except Exception:
7475
assert a == b, msg
7576

7677

77-
def assert_image(im, mode, size, msg=None):
78+
def assert_image(
79+
im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None
80+
) -> None:
7881
if mode is not None:
7982
assert im.mode == mode, (
8083
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
@@ -86,28 +89,32 @@ def assert_image(im, mode, size, msg=None):
8689
)
8790

8891

89-
def assert_image_equal(a, b, msg=None):
92+
def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None:
9093
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
9194
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
9295
if a.tobytes() != b.tobytes():
93-
if HAS_UPLOADER:
94-
try:
95-
url = test_image_results.upload(a, b)
96+
try:
97+
url = upload(a, b)
98+
if url:
9699
logger.error("URL for test images: %s", url)
97-
except Exception:
98-
pass
100+
except Exception:
101+
pass
99102

100103
pytest.fail(msg or "got different content")
101104

102105

103-
def assert_image_equal_tofile(a, filename, msg=None, mode=None):
106+
def assert_image_equal_tofile(
107+
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
108+
) -> None:
104109
with Image.open(filename) as img:
105110
if mode:
106111
img = img.convert(mode)
107112
assert_image_equal(a, img, msg)
108113

109114

110-
def assert_image_similar(a, b, epsilon, msg=None):
115+
def assert_image_similar(
116+
a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None
117+
) -> None:
111118
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
112119
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
113120

@@ -125,55 +132,68 @@ def assert_image_similar(a, b, epsilon, msg=None):
125132
+ f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
126133
)
127134
except Exception as e:
128-
if HAS_UPLOADER:
129-
try:
130-
url = test_image_results.upload(a, b)
135+
try:
136+
url = upload(a, b)
137+
if url:
131138
logger.exception("URL for test images: %s", url)
132-
except Exception:
133-
pass
139+
except Exception:
140+
pass
134141
raise e
135142

136143

137-
def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None):
144+
def assert_image_similar_tofile(
145+
a: Image.Image,
146+
filename: str,
147+
epsilon: float,
148+
msg: str | None = None,
149+
mode: str | None = None,
150+
) -> None:
138151
with Image.open(filename) as img:
139152
if mode:
140153
img = img.convert(mode)
141154
assert_image_similar(a, img, epsilon, msg)
142155

143156

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

147160

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

151164

152-
def assert_tuple_approx_equal(actuals, targets, threshold, msg):
165+
def assert_tuple_approx_equal(
166+
actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str
167+
) -> None:
153168
"""Tests if actuals has values within threshold from targets"""
154-
value = True
155169
for i, target in enumerate(targets):
156-
value *= target - threshold <= actuals[i] <= target + threshold
157-
158-
assert value, msg + ": " + repr(actuals) + " != " + repr(targets)
170+
if not (target - threshold <= actuals[i] <= target + threshold):
171+
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
159172

160173

161174
def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
162175
reason = f"{feature} not available"
163176
return pytest.mark.skipif(not features.check(feature), reason=reason)
164177

165178

166-
def skip_unless_feature_version(feature, version_required, reason=None):
179+
def skip_unless_feature_version(
180+
feature: str, required: str, reason: str | None = None
181+
) -> pytest.MarkDecorator:
167182
if not features.check(feature):
168183
return pytest.mark.skip(f"{feature} not available")
169184
if reason is None:
170-
reason = f"{feature} is older than {version_required}"
171-
version_required = parse_version(version_required)
185+
reason = f"{feature} is older than {required}"
186+
version_required = parse_version(required)
172187
version_available = parse_version(features.version(feature))
173188
return pytest.mark.skipif(version_available < version_required, reason=reason)
174189

175190

176-
def mark_if_feature_version(mark, feature, version_blacklist, reason=None):
191+
def mark_if_feature_version(
192+
mark: pytest.MarkDecorator,
193+
feature: str,
194+
version_blacklist: str,
195+
reason: str | None = None,
196+
) -> pytest.MarkDecorator:
177197
if not features.check(feature):
178198
return pytest.mark.pil_noop_mark()
179199
if reason is None:
@@ -194,7 +214,7 @@ class PillowLeakTestCase:
194214
iterations = 100 # count
195215
mem_limit = 512 # k
196216

197-
def _get_mem_usage(self):
217+
def _get_mem_usage(self) -> float:
198218
"""
199219
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
200220
between macOS and Linux rss reporting
@@ -216,7 +236,7 @@ def _get_mem_usage(self):
216236
# This is the maximum resident set size used (in kilobytes).
217237
return mem # Kb
218238

219-
def _test_leak(self, core):
239+
def _test_leak(self, core: Callable[[], None]) -> None:
220240
start_mem = self._get_mem_usage()
221241
for cycle in range(self.iterations):
222242
core()
@@ -228,17 +248,17 @@ def _test_leak(self, core):
228248
# helpers
229249

230250

231-
def fromstring(data):
251+
def fromstring(data: bytes) -> Image.Image:
232252
return Image.open(BytesIO(data))
233253

234254

235-
def tostring(im, string_format, **options):
255+
def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
236256
out = BytesIO()
237257
im.save(out, string_format, **options)
238258
return out.getvalue()
239259

240260

241-
def hopper(mode=None, cache={}):
261+
def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
242262
if mode is None:
243263
# Always return fresh not-yet-loaded version of image.
244264
# Operations on not-yet-loaded images is separate class of errors
@@ -259,29 +279,31 @@ def hopper(mode=None, cache={}):
259279
return im.copy()
260280

261281

262-
def djpeg_available():
282+
def djpeg_available() -> bool:
263283
if shutil.which("djpeg"):
264284
try:
265285
subprocess.check_call(["djpeg", "-version"])
266286
return True
267287
except subprocess.CalledProcessError: # pragma: no cover
268288
return False
289+
return False
269290

270291

271-
def cjpeg_available():
292+
def cjpeg_available() -> bool:
272293
if shutil.which("cjpeg"):
273294
try:
274295
subprocess.check_call(["cjpeg", "-version"])
275296
return True
276297
except subprocess.CalledProcessError: # pragma: no cover
277298
return False
299+
return False
278300

279301

280-
def netpbm_available():
302+
def netpbm_available() -> bool:
281303
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
282304

283305

284-
def magick_command():
306+
def magick_command() -> list[str] | None:
285307
if sys.platform == "win32":
286308
magickhome = os.environ.get("MAGICK_HOME")
287309
if magickhome:
@@ -298,47 +320,48 @@ def magick_command():
298320
return imagemagick
299321
if graphicsmagick and shutil.which(graphicsmagick[0]):
300322
return graphicsmagick
323+
return None
301324

302325

303-
def on_appveyor():
326+
def on_appveyor() -> bool:
304327
return "APPVEYOR" in os.environ
305328

306329

307-
def on_github_actions():
330+
def on_github_actions() -> bool:
308331
return "GITHUB_ACTIONS" in os.environ
309332

310333

311-
def on_ci():
334+
def on_ci() -> bool:
312335
# GitHub Actions and AppVeyor have "CI"
313336
return "CI" in os.environ
314337

315338

316-
def is_big_endian():
339+
def is_big_endian() -> bool:
317340
return sys.byteorder == "big"
318341

319342

320-
def is_ppc64le():
343+
def is_ppc64le() -> bool:
321344
import platform
322345

323346
return platform.machine() == "ppc64le"
324347

325348

326-
def is_win32():
349+
def is_win32() -> bool:
327350
return sys.platform.startswith("win32")
328351

329352

330-
def is_pypy():
353+
def is_pypy() -> bool:
331354
return hasattr(sys, "pypy_translation_info")
332355

333356

334-
def is_mingw():
357+
def is_mingw() -> bool:
335358
return sysconfig.get_platform() == "mingw"
336359

337360

338361
class CachedProperty:
339-
def __init__(self, func):
362+
def __init__(self, func: Callable[[Any], None]) -> None:
340363
self.func = func
341364

342-
def __get__(self, instance, cls=None):
365+
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
343366
result = instance.__dict__[self.func.__name__] = self.func(instance)
344367
return result

0 commit comments

Comments
 (0)