1414
1515import pytest
1616from packaging .version import parse as parse_version
17+ from typing import Any , Callable , Sequence
1718
1819from PIL import Image , ImageMath , features
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,16 @@ 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 (a : Any , b : Any , msg : str | None = None ) -> None :
7170 try :
7271 assert len (a ) == len (b ), msg or f"got length { len (a )} , expected { len (b )} "
7372 except Exception :
7473 assert a == b , msg
7574
7675
77- def assert_image (im , mode , size , msg = None ):
76+ def assert_image (
77+ im : Image .Image , mode : str , size : tuple [int , int ], msg : str | None = None
78+ ) -> None :
7879 if mode is not None :
7980 assert im .mode == mode , (
8081 msg or f"got mode { repr (im .mode )} , expected { repr (mode )} "
@@ -86,28 +87,32 @@ def assert_image(im, mode, size, msg=None):
8687 )
8788
8889
89- def assert_image_equal (a , b , msg = None ):
90+ def assert_image_equal (a : Image . Image , b : Image . Image , msg : str | None = None ) -> None :
9091 assert a .mode == b .mode , msg or f"got mode { repr (a .mode )} , expected { repr (b .mode )} "
9192 assert a .size == b .size , msg or f"got size { repr (a .size )} , expected { repr (b .size )} "
9293 if a .tobytes () != b .tobytes ():
93- if HAS_UPLOADER :
94+ if uploader :
9495 try :
95- url = test_image_results . upload (a , b )
96+ url = upload (a , b )
9697 logger .error ("URL for test images: %s" , url )
9798 except Exception :
9899 pass
99100
100101 pytest .fail (msg or "got different content" )
101102
102103
103- def assert_image_equal_tofile (a , filename , msg = None , mode = None ):
104+ def assert_image_equal_tofile (
105+ a : Image .Image , filename : str , msg : str | None = None , mode : str | None = None
106+ ) -> None :
104107 with Image .open (filename ) as img :
105108 if mode :
106109 img = img .convert (mode )
107110 assert_image_equal (a , img , msg )
108111
109112
110- def assert_image_similar (a , b , epsilon , msg = None ):
113+ def assert_image_similar (
114+ a : Image .Image , b : Image .Image , epsilon : float , msg : str | None = None
115+ ) -> None :
111116 assert a .mode == b .mode , msg or f"got mode { repr (a .mode )} , expected { repr (b .mode )} "
112117 assert a .size == b .size , msg or f"got size { repr (a .size )} , expected { repr (b .size )} "
113118
@@ -125,55 +130,68 @@ def assert_image_similar(a, b, epsilon, msg=None):
125130 + f" average pixel value difference { ave_diff :.4f} > epsilon { epsilon :.4f} "
126131 )
127132 except Exception as e :
128- if HAS_UPLOADER :
133+ if uploader :
129134 try :
130- url = test_image_results . upload (a , b )
135+ url = upload (a , b )
131136 logger .exception ("URL for test images: %s" , url )
132137 except Exception :
133138 pass
134139 raise e
135140
136141
137- def assert_image_similar_tofile (a , filename , epsilon , msg = None , mode = None ):
142+ def assert_image_similar_tofile (
143+ a : Image .Image ,
144+ filename : str ,
145+ epsilon : float ,
146+ msg : str | None = None ,
147+ mode : str | None = None ,
148+ ) -> None :
138149 with Image .open (filename ) as img :
139150 if mode :
140151 img = img .convert (mode )
141152 assert_image_similar (a , img , epsilon , msg )
142153
143154
144- def assert_all_same (items , msg = None ):
155+ def assert_all_same (items : Sequence [ Any ] , msg : str | None = None ) -> None :
145156 assert items .count (items [0 ]) == len (items ), msg
146157
147158
148- def assert_not_all_same (items , msg = None ):
159+ def assert_not_all_same (items : Sequence [ Any ] , msg : str | None = None ) -> None :
149160 assert items .count (items [0 ]) != len (items ), msg
150161
151162
152- def assert_tuple_approx_equal (actuals , targets , threshold , msg ):
163+ def assert_tuple_approx_equal (
164+ actuals : Sequence [int ], targets : tuple [int , ...], threshold : int , msg : str
165+ ) -> None :
153166 """Tests if actuals has values within threshold from targets"""
154- value = True
155167 for i , target in enumerate (targets ):
156- value *= target - threshold <= actuals [i ] <= target + threshold
157-
158- assert value , msg + ": " + repr (actuals ) + " != " + repr (targets )
168+ if not (target - threshold <= actuals [i ] <= target + threshold ):
169+ pytest .fail (msg + ": " + repr (actuals ) + " != " + repr (targets ))
159170
160171
161172def skip_unless_feature (feature : str ) -> pytest .MarkDecorator :
162173 reason = f"{ feature } not available"
163174 return pytest .mark .skipif (not features .check (feature ), reason = reason )
164175
165176
166- def skip_unless_feature_version (feature , version_required , reason = None ):
177+ def skip_unless_feature_version (
178+ feature : str , required : str , reason : str | None = None
179+ ) -> pytest .MarkDecorator :
167180 if not features .check (feature ):
168181 return pytest .mark .skip (f"{ feature } not available" )
169182 if reason is None :
170- reason = f"{ feature } is older than { version_required } "
171- version_required = parse_version (version_required )
183+ reason = f"{ feature } is older than { required } "
184+ version_required = parse_version (required )
172185 version_available = parse_version (features .version (feature ))
173186 return pytest .mark .skipif (version_available < version_required , reason = reason )
174187
175188
176- def mark_if_feature_version (mark , feature , version_blacklist , reason = None ):
189+ def mark_if_feature_version (
190+ mark : pytest .MarkDecorator ,
191+ feature : str ,
192+ version_blacklist : str ,
193+ reason : str | None = None ,
194+ ) -> pytest .MarkDecorator :
177195 if not features .check (feature ):
178196 return pytest .mark .pil_noop_mark ()
179197 if reason is None :
@@ -194,7 +212,7 @@ class PillowLeakTestCase:
194212 iterations = 100 # count
195213 mem_limit = 512 # k
196214
197- def _get_mem_usage (self ):
215+ def _get_mem_usage (self ) -> float :
198216 """
199217 Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
200218 between macOS and Linux rss reporting
@@ -216,7 +234,7 @@ def _get_mem_usage(self):
216234 # This is the maximum resident set size used (in kilobytes).
217235 return mem # Kb
218236
219- def _test_leak (self , core ) :
237+ def _test_leak (self , core : Callable [[], None ]) -> None :
220238 start_mem = self ._get_mem_usage ()
221239 for cycle in range (self .iterations ):
222240 core ()
@@ -228,17 +246,17 @@ def _test_leak(self, core):
228246# helpers
229247
230248
231- def fromstring (data ) :
249+ def fromstring (data : bytes ) -> Image . Image :
232250 return Image .open (BytesIO (data ))
233251
234252
235- def tostring (im , string_format , ** options ) :
253+ def tostring (im : Image . Image , string_format : str , ** options : dict [ str , Any ]) -> bytes :
236254 out = BytesIO ()
237255 im .save (out , string_format , ** options )
238256 return out .getvalue ()
239257
240258
241- def hopper (mode = None , cache = {}):
259+ def hopper (mode : str | None = None , cache : dict [ str , Image . Image ] = {}) -> Image . Image :
242260 if mode is None :
243261 # Always return fresh not-yet-loaded version of image.
244262 # Operations on not-yet-loaded images is separate class of errors
@@ -259,29 +277,31 @@ def hopper(mode=None, cache={}):
259277 return im .copy ()
260278
261279
262- def djpeg_available ():
280+ def djpeg_available () -> bool :
263281 if shutil .which ("djpeg" ):
264282 try :
265283 subprocess .check_call (["djpeg" , "-version" ])
266284 return True
267285 except subprocess .CalledProcessError : # pragma: no cover
268- return False
286+ pass
287+ return False
269288
270289
271- def cjpeg_available ():
290+ def cjpeg_available () -> bool :
272291 if shutil .which ("cjpeg" ):
273292 try :
274293 subprocess .check_call (["cjpeg" , "-version" ])
275294 return True
276295 except subprocess .CalledProcessError : # pragma: no cover
277- return False
296+ pass
297+ return False
278298
279299
280- def netpbm_available ():
300+ def netpbm_available () -> bool :
281301 return bool (shutil .which ("ppmquant" ) and shutil .which ("ppmtogif" ))
282302
283303
284- def magick_command ():
304+ def magick_command () -> list [ str ] | None :
285305 if sys .platform == "win32" :
286306 magickhome = os .environ .get ("MAGICK_HOME" )
287307 if magickhome :
@@ -298,47 +318,48 @@ def magick_command():
298318 return imagemagick
299319 if graphicsmagick and shutil .which (graphicsmagick [0 ]):
300320 return graphicsmagick
321+ return None
301322
302323
303- def on_appveyor ():
324+ def on_appveyor () -> bool :
304325 return "APPVEYOR" in os .environ
305326
306327
307- def on_github_actions ():
328+ def on_github_actions () -> bool :
308329 return "GITHUB_ACTIONS" in os .environ
309330
310331
311- def on_ci ():
332+ def on_ci () -> bool :
312333 # GitHub Actions and AppVeyor have "CI"
313334 return "CI" in os .environ
314335
315336
316- def is_big_endian ():
337+ def is_big_endian () -> bool :
317338 return sys .byteorder == "big"
318339
319340
320- def is_ppc64le ():
341+ def is_ppc64le () -> bool :
321342 import platform
322343
323344 return platform .machine () == "ppc64le"
324345
325346
326- def is_win32 ():
347+ def is_win32 () -> bool :
327348 return sys .platform .startswith ("win32" )
328349
329350
330- def is_pypy ():
351+ def is_pypy () -> bool :
331352 return hasattr (sys , "pypy_translation_info" )
332353
333354
334- def is_mingw ():
355+ def is_mingw () -> bool :
335356 return sysconfig .get_platform () == "mingw"
336357
337358
338359class CachedProperty :
339- def __init__ (self , func ) :
360+ def __init__ (self , func : Callable [[ Any ], None ]) -> None :
340361 self .func = func
341362
342- def __get__ (self , instance , cls = None ):
363+ def __get__ (self , instance : Any , cls : type [ Any ] | None = None ) -> Any :
343364 result = instance .__dict__ [self .func .__name__ ] = self .func (instance )
344365 return result
0 commit comments