@@ -2761,6 +2761,73 @@ class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests,
27612761 compression = zipfile .ZIP_ZSTANDARD
27622762
27632763
2764+ class MonkeypatchedDecompressorTests (unittest .TestCase ):
2765+ # Some third-party projects monkey-patch _get_decompressor() to add
2766+ # additional compression schemes. This can break at any time as the
2767+ # internal compressor objects change.
2768+ # To protect users, we try to keep this case working.
2769+ # See also: GH-156002 and GH-113767.
2770+ COMPRESSION = 99
2771+
2772+ class Compressor :
2773+ """Compressor with only the original BZ2Compressor API"""
2774+ def compress (self , data ):
2775+ return data .swapcase ()
2776+
2777+ def flush (self ):
2778+ return b''
2779+
2780+ class Decompressor :
2781+ """Decompressor with only the 3.3+ BZ2Decompressor API"""
2782+ eof = False
2783+
2784+ def decompress (self , data ):
2785+ return data .swapcase ()
2786+
2787+ def setUp (self ):
2788+ orig_check_compression = zipfile ._check_compression
2789+ orig_get_compressor = zipfile ._get_compressor
2790+ orig_get_decompressor = zipfile ._get_decompressor
2791+
2792+ def check_compression (compression ):
2793+ if compression != self .COMPRESSION :
2794+ orig_check_compression (compression )
2795+
2796+ def get_compressor (compress_type , compresslevel = None ):
2797+ if compress_type == self .COMPRESSION :
2798+ return self .Compressor ()
2799+ return orig_get_compressor (compress_type , compresslevel )
2800+
2801+ def get_decompressor (compress_type ):
2802+ if compress_type == self .COMPRESSION :
2803+ return self .Decompressor ()
2804+ return orig_get_decompressor (compress_type )
2805+
2806+ self .enterContext (mock .patch .object (
2807+ zipfile , '_check_compression' , check_compression ))
2808+ self .enterContext (mock .patch .object (
2809+ zipfile , '_get_compressor' , get_compressor ))
2810+ self .enterContext (mock .patch .object (
2811+ zipfile , '_get_decompressor' , get_decompressor ))
2812+
2813+ def test_roundtrip_monkeypatched_decompressor (self ):
2814+ data = bytes (range (256 )) * 8
2815+ buf = io .BytesIO ()
2816+ with zipfile .ZipFile (buf , "w" , compression = self .COMPRESSION ) as zf :
2817+ zf .writestr ("member" , data )
2818+ self .assertIn (data .swapcase (), buf .getvalue ())
2819+ with zipfile .ZipFile (io .BytesIO (buf .getvalue ())) as zf :
2820+ self .assertEqual (zf .read ("member" ), data )
2821+ with zf .open ("member" ) as f :
2822+ self .assertEqual (f .read (100 ), data [:100 ])
2823+ self .assertEqual (f .read1 (100 ), data [100 :200 ])
2824+ f .seek (- 100 , os .SEEK_END )
2825+ self .assertEqual (f .read (), data [- 100 :])
2826+ # Rewinding past the read buffer re-creates the decompressor.
2827+ f .seek (0 )
2828+ self .assertEqual (f .read (), data )
2829+
2830+
27642831class AbstractBadCrcTests :
27652832 def test_testzip_with_bad_crc (self ):
27662833 """Tests that files with bad CRCs return their name from testzip."""
0 commit comments