@@ -3653,6 +3653,178 @@ def test_large_utf8_input(self, size):
36533653 self .assertEqual (decoded [0 ][- 11 :], '56\ud100 0123456\ud100 ' )
36543654
36553655
3656+ def iconv_encoding_available (name ):
3657+ # The encodings iconv provides are platform-dependent, so tests must probe
3658+ # availability rather than assume it.
3659+ try :
3660+ codecs .iconv_encode (name , '' )
3661+ except (LookupError , OSError ):
3662+ return False
3663+ return True
3664+
3665+
3666+ # Candidate encodings with sample text; tests probe several and skip only when a
3667+ # whole category is unavailable.
3668+ _ICONV_SINGLE_BYTE = [
3669+ ('KOI8-U' , 'Привіт, світ!' ),
3670+ ('ISO-8859-7' , 'Καλημέρα' ),
3671+ ('ISO-8859-2' , 'Zażółć gęślą jaźń' ),
3672+ ('ISO-8859-1' , 'Grüße' ),
3673+ ]
3674+ _ICONV_MULTIBYTE = ['EUC-JP' , 'SHIFT_JIS' , 'GBK' , 'GB18030' , 'BIG5' ]
3675+ # Encodings iconv may provide but for which CPython has no built-in codec
3676+ # (cp1047 is EBCDIC, i.e. not ASCII-compatible).
3677+ _ICONV_ONLY = ['cp1047' , 'cp1133' , 'GEORGIAN-PS' , 'ARMSCII-8' ]
3678+
3679+
3680+ @unittest .skipUnless (hasattr (codecs , 'iconv_encode' ),
3681+ 'the iconv codec is not available' )
3682+ class IconvTest (unittest .TestCase ):
3683+
3684+ def require (self , * names ):
3685+ for name in names :
3686+ if iconv_encoding_available (name ):
3687+ return name
3688+ self .skipTest ('no suitable iconv encoding is available' )
3689+
3690+ def require_single_byte (self ):
3691+ for enc , text in _ICONV_SINGLE_BYTE :
3692+ if iconv_encoding_available (enc ):
3693+ return enc , text
3694+ self .skipTest ('no single-byte iconv encoding is available' )
3695+
3696+ def test_unknown_encoding (self ):
3697+ self .assertRaises (LookupError , codecs .iconv_encode , 'no-such-enc-42' , 'a' )
3698+ self .assertRaises (LookupError , codecs .iconv_decode , 'no-such-enc-42' , b'a' )
3699+ self .assertRaises (LookupError , codecs .lookup , 'iconv:no-such-enc-42' )
3700+
3701+ def test_roundtrip (self ):
3702+ cases = _ICONV_SINGLE_BYTE + [(enc , '日本語' ) for enc in _ICONV_MULTIBYTE ]
3703+ tested = False
3704+ for enc , text in cases :
3705+ if not iconv_encoding_available (enc ):
3706+ continue
3707+ tested = True
3708+ with self .subTest (encoding = enc ):
3709+ data = codecs .iconv_encode (enc , text )[0 ]
3710+ decoded , consumed = codecs .iconv_decode (enc , data , 'strict' , True )
3711+ self .assertEqual (decoded , text )
3712+ self .assertEqual (consumed , len (data ))
3713+ if not tested :
3714+ self .skipTest ('none of the test encodings are available' )
3715+
3716+ def test_encode_errors (self ):
3717+ # A non-ASCII character is not representable in ASCII.
3718+ enc = self .require ('ASCII' )
3719+ with self .assertRaises (UnicodeEncodeError ) as cm :
3720+ codecs .iconv_encode (enc , 'a€b' )
3721+ self .assertEqual ((cm .exception .encoding , cm .exception .start ,
3722+ cm .exception .end ), (enc , 1 , 2 ))
3723+ self .assertEqual (codecs .iconv_encode (enc , 'a€b' , 'replace' )[0 ], b'a?b' )
3724+ self .assertEqual (codecs .iconv_encode (enc , 'a€b' , 'ignore' )[0 ], b'ab' )
3725+ self .assertEqual (codecs .iconv_encode (enc , 'a€b' , 'backslashreplace' )[0 ],
3726+ b'a\\ u20acb' )
3727+ self .assertEqual (codecs .iconv_encode (enc , 'a€b' , 'xmlcharrefreplace' )[0 ],
3728+ b'a€b' )
3729+
3730+ def test_decode_errors (self ):
3731+ enc = self .require ('ASCII' )
3732+ bad = b'a\xff b'
3733+ with self .assertRaises (UnicodeDecodeError ) as cm :
3734+ codecs .iconv_decode (enc , bad , 'strict' , True )
3735+ self .assertEqual ((cm .exception .encoding , cm .exception .start ), (enc , 1 ))
3736+ self .assertEqual (codecs .iconv_decode (enc , bad , 'replace' , True )[0 ],
3737+ 'a\ufffd b' )
3738+ self .assertEqual (codecs .iconv_decode (enc , bad , 'ignore' , True )[0 ], 'ab' )
3739+ self .assertEqual (codecs .iconv_decode (enc , bad , 'backslashreplace' , True )[0 ],
3740+ 'a\\ xffb' )
3741+
3742+ def test_stateful_decode (self ):
3743+ enc = self .require (* _ICONV_MULTIBYTE )
3744+ full = codecs .iconv_encode (enc , '日本' )[0 ]
3745+ # A trailing incomplete multibyte sequence is deferred when final is
3746+ # false, and reported through *consumed*.
3747+ text , consumed = codecs .iconv_decode (enc , full [:- 1 ], 'strict' , False )
3748+ self .assertEqual (text , '日' )
3749+ self .assertEqual (consumed , len (full ) - 2 )
3750+ # With final=True the same input is an error.
3751+ self .assertRaises (UnicodeDecodeError ,
3752+ codecs .iconv_decode , enc , full [:- 1 ], 'strict' , True )
3753+
3754+ def test_empty (self ):
3755+ enc = self .require ('ASCII' )
3756+ self .assertEqual (codecs .iconv_encode (enc , '' ), (b'' , 0 ))
3757+ self .assertEqual (codecs .iconv_decode (enc , b'' , 'strict' , True ), ('' , 0 ))
3758+
3759+ def test_lookup_bare_name (self ):
3760+ # An encoding that iconv knows but Python has no built-in codec for.
3761+ for name in _ICONV_ONLY :
3762+ if (iconv_encoding_available (name )
3763+ and encodings .search_function (name ) is None ):
3764+ break
3765+ else :
3766+ self .skipTest ('no iconv-only encoding is available' )
3767+ info = codecs .lookup (name )
3768+ self .assertEqual (info .name , name .lower ())
3769+ # The encoding need not be ASCII-compatible (e.g. EBCDIC), so just
3770+ # check that it round-trips.
3771+ self .assertEqual ('abc' .encode (name ).decode (name ), 'abc' )
3772+
3773+ def test_lookup_does_not_shadow_builtin (self ):
3774+ # Built-in codecs must win over the iconv fallback.
3775+ self .assertEqual (codecs .lookup ('utf-8' ).name , 'utf-8' )
3776+ self .assertEqual (codecs .lookup ('ascii' ).name , 'ascii' )
3777+
3778+ def test_iconv_prefix_forces_engine (self ):
3779+ # These candidates all have a built-in codec to compare against.
3780+ enc , text = self .require_single_byte ()
3781+ info = codecs .lookup ('iconv:' + enc )
3782+ # The registry lower-cases the requested name.
3783+ self .assertEqual (info .name , ('iconv:' + enc ).lower ())
3784+ self .assertEqual (text .encode ('iconv:' + enc ), text .encode (enc ))
3785+ self .assertEqual (text .encode ('iconv:' + enc ).decode ('iconv:' + enc ), text )
3786+
3787+ def test_incremental_decode (self ):
3788+ enc = self .require (* _ICONV_MULTIBYTE )
3789+ text = '日本語'
3790+ data = codecs .encode (text , 'iconv:' + enc )
3791+ dec = codecs .getincrementaldecoder ('iconv:' + enc )()
3792+ out = '' .join (dec .decode (data [i :i + 1 ]) for i in range (len (data )))
3793+ out += dec .decode (b'' , True )
3794+ self .assertEqual (out , text )
3795+
3796+ def test_stream (self ):
3797+ enc = self .require (* _ICONV_MULTIBYTE )
3798+ text = '日本語'
3799+ raw = codecs .encode (text , 'iconv:' + enc )
3800+ reader = codecs .getreader ('iconv:' + enc )(io .BytesIO (raw ))
3801+ self .assertEqual (reader .read (), text )
3802+
3803+ def test_encode_kinds (self ):
3804+ # The string's own buffer is fed to iconv per storage kind; check each
3805+ # of the 1-, 2- and 4-byte kinds against the built-in codec.
3806+ enc = self .require ('UTF-8' )
3807+ for text in ('Gr\xfc \xdf e' , 'ĀāĂ' , 'A\U0001f389 B' ):
3808+ with self .subTest (text = text ):
3809+ self .assertEqual (text .encode ('iconv:' + enc ), text .encode (enc ))
3810+
3811+ def test_encode_surrogateescape (self ):
3812+ # A lone surrogate lives in the 2-byte kind and round-trips.
3813+ enc = self .require ('ASCII' )
3814+ data = b'ab\xff '
3815+ s = data .decode (enc , 'surrogateescape' )
3816+ self .assertEqual (s .encode ('iconv:' + enc , 'surrogateescape' ), data )
3817+
3818+ def test_encode_surrogate_pair (self ):
3819+ # A surrogate pair must stay two code points, never combined into an
3820+ # astral character (as UTF-16 would): backslashreplace escapes each
3821+ # surrogate separately, not the escape of a single combined character.
3822+ pair = '\ud83c \udf89 '
3823+ latin1 = self .require ('ISO-8859-1' , 'ASCII' )
3824+ self .assertEqual (pair .encode ('iconv:' + latin1 , 'backslashreplace' ),
3825+ rb'\ud83c\udf89' )
3826+
3827+
36563828class ASCIITest (unittest .TestCase ):
36573829 def test_encode (self ):
36583830 self .assertEqual ('abc123' .encode ('ascii' ), b'abc123' )
0 commit comments