1111import sysconfig
1212import tempfile
1313from io import BytesIO
14+ from typing import Any , Callable , Sequence
1415
1516import pytest
1617from packaging .version import parse as parse_version
1920
2021logger = logging .getLogger (__name__ )
2122
22-
23- HAS_UPLOADER = False
24-
23+ uploader = None
2524if 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"
3526elif "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"
4828else :
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
161174def 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
338361class 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