Skip to content

Commit e1ab979

Browse files
committed
#9200: The str.is* methods now work with strings that contain non-BMP characters even in narrow Unicode builds.
1 parent dce77ce commit e1ab979

3 files changed

Lines changed: 220 additions & 42 deletions

File tree

Lib/test/test_unicode.py

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,26 +345,69 @@ def test_fixup(s):
345345
def test_islower(self):
346346
string_tests.MixinStrUnicodeUserStringTest.test_islower(self)
347347
self.checkequalnofix(False, '\u1FFc', 'islower')
348+
# non-BMP, uppercase
349+
self.assertFalse('\U00010401'.islower())
350+
self.assertFalse('\U00010427'.islower())
351+
# non-BMP, lowercase
352+
self.assertTrue('\U00010429'.islower())
353+
self.assertTrue('\U0001044E'.islower())
354+
# non-BMP, non-cased
355+
self.assertFalse('\U0001F40D'.islower())
356+
self.assertFalse('\U0001F46F'.islower())
348357

349358
def test_isupper(self):
350359
string_tests.MixinStrUnicodeUserStringTest.test_isupper(self)
351360
if not sys.platform.startswith('java'):
352361
self.checkequalnofix(False, '\u1FFc', 'isupper')
362+
# non-BMP, uppercase
363+
self.assertTrue('\U00010401'.isupper())
364+
self.assertTrue('\U00010427'.isupper())
365+
# non-BMP, lowercase
366+
self.assertFalse('\U00010429'.isupper())
367+
self.assertFalse('\U0001044E'.isupper())
368+
# non-BMP, non-cased
369+
self.assertFalse('\U0001F40D'.isupper())
370+
self.assertFalse('\U0001F46F'.isupper())
353371

354372
def test_istitle(self):
355-
string_tests.MixinStrUnicodeUserStringTest.test_title(self)
373+
string_tests.MixinStrUnicodeUserStringTest.test_istitle(self)
356374
self.checkequalnofix(True, '\u1FFc', 'istitle')
357375
self.checkequalnofix(True, 'Greek \u1FFcitlecases ...', 'istitle')
358376

377+
# non-BMP, uppercase + lowercase
378+
self.assertTrue('\U00010401\U00010429'.istitle())
379+
self.assertTrue('\U00010427\U0001044E'.istitle())
380+
# apparently there are no titlecased (Lt) non-BMP chars in Unicode 6
381+
for ch in ['\U00010429', '\U0001044E', '\U0001F40D', '\U0001F46F']:
382+
self.assertFalse(ch.istitle(), '{!a} is not title'.format(ch))
383+
359384
def test_isspace(self):
360385
string_tests.MixinStrUnicodeUserStringTest.test_isspace(self)
361386
self.checkequalnofix(True, '\u2000', 'isspace')
362387
self.checkequalnofix(True, '\u200a', 'isspace')
363388
self.checkequalnofix(False, '\u2014', 'isspace')
389+
# apparently there are no non-BMP spaces chars in Unicode 6
390+
for ch in ['\U00010401', '\U00010427', '\U00010429', '\U0001044E',
391+
'\U0001F40D', '\U0001F46F']:
392+
self.assertFalse(ch.isspace(), '{!a} is not space.'.format(ch))
393+
394+
def test_isalnum(self):
395+
string_tests.MixinStrUnicodeUserStringTest.test_isalnum(self)
396+
for ch in ['\U00010401', '\U00010427', '\U00010429', '\U0001044E',
397+
'\U0001D7F6', '\U00011066', '\U000104A0', '\U0001F107']:
398+
self.assertTrue(ch.isalnum(), '{!a} is alnum.'.format(ch))
364399

365400
def test_isalpha(self):
366401
string_tests.MixinStrUnicodeUserStringTest.test_isalpha(self)
367402
self.checkequalnofix(True, '\u1FFc', 'isalpha')
403+
# non-BMP, cased
404+
self.assertTrue('\U00010401'.isalpha())
405+
self.assertTrue('\U00010427'.isalpha())
406+
self.assertTrue('\U00010429'.isalpha())
407+
self.assertTrue('\U0001044E'.isalpha())
408+
# non-BMP, non-cased
409+
self.assertFalse('\U0001F40D'.isalpha())
410+
self.assertFalse('\U0001F46F'.isalpha())
368411

369412
def test_isdecimal(self):
370413
self.checkequalnofix(False, '', 'isdecimal')
@@ -378,12 +421,24 @@ def test_isdecimal(self):
378421

379422
self.checkraises(TypeError, 'abc', 'isdecimal', 42)
380423

424+
for ch in ['\U00010401', '\U00010427', '\U00010429', '\U0001044E',
425+
'\U0001F40D', '\U0001F46F', '\U00011065', '\U0001F107']:
426+
self.assertFalse(ch.isdecimal(), '{!a} is not decimal.'.format(ch))
427+
for ch in ['\U0001D7F6', '\U00011066', '\U000104A0']:
428+
self.assertTrue(ch.isdecimal(), '{!a} is decimal.'.format(ch))
429+
381430
def test_isdigit(self):
382431
string_tests.MixinStrUnicodeUserStringTest.test_isdigit(self)
383432
self.checkequalnofix(True, '\u2460', 'isdigit')
384433
self.checkequalnofix(False, '\xbc', 'isdigit')
385434
self.checkequalnofix(True, '\u0660', 'isdigit')
386435

436+
for ch in ['\U00010401', '\U00010427', '\U00010429', '\U0001044E',
437+
'\U0001F40D', '\U0001F46F', '\U00011065']:
438+
self.assertFalse(ch.isdigit(), '{!a} is not a digit.'.format(ch))
439+
for ch in ['\U0001D7F6', '\U00011066', '\U000104A0', '\U0001F107']:
440+
self.assertTrue(ch.isdigit(), '{!a} is a digit.'.format(ch))
441+
387442
def test_isnumeric(self):
388443
self.checkequalnofix(False, '', 'isnumeric')
389444
self.checkequalnofix(False, 'a', 'isnumeric')
@@ -396,6 +451,13 @@ def test_isnumeric(self):
396451

397452
self.assertRaises(TypeError, "abc".isnumeric, 42)
398453

454+
for ch in ['\U00010401', '\U00010427', '\U00010429', '\U0001044E',
455+
'\U0001F40D', '\U0001F46F']:
456+
self.assertFalse(ch.isnumeric(), '{!a} is not numeric.'.format(ch))
457+
for ch in ['\U00011065', '\U0001D7F6', '\U00011066',
458+
'\U000104A0', '\U0001F107']:
459+
self.assertTrue(ch.isnumeric(), '{!a} is numeric.'.format(ch))
460+
399461
def test_isidentifier(self):
400462
self.assertTrue("a".isidentifier())
401463
self.assertTrue("Z".isidentifier())
@@ -423,6 +485,100 @@ def test_isprintable(self):
423485
# single surrogate character
424486
self.assertFalse("\ud800".isprintable())
425487

488+
self.assertTrue('\U0001F46F'.isprintable())
489+
self.assertFalse('\U000E0020'.isprintable())
490+
491+
def test_surrogates(self):
492+
for s in ('a\uD800b\uDFFF', 'a\uDFFFb\uD800',
493+
'a\uD800b\uDFFFa', 'a\uDFFFb\uD800a'):
494+
self.assertTrue(s.islower())
495+
self.assertFalse(s.isupper())
496+
self.assertFalse(s.istitle())
497+
for s in ('A\uD800B\uDFFF', 'A\uDFFFB\uD800',
498+
'A\uD800B\uDFFFA', 'A\uDFFFB\uD800A'):
499+
self.assertFalse(s.islower())
500+
self.assertTrue(s.isupper())
501+
self.assertTrue(s.istitle())
502+
503+
for meth_name in ('islower', 'isupper', 'istitle'):
504+
meth = getattr(str, meth_name)
505+
for s in ('\uD800', '\uDFFF', '\uD800\uD800', '\uDFFF\uDFFF'):
506+
self.assertFalse(meth(s), '%a.%s() is False' % (s, meth_name))
507+
508+
for meth_name in ('isalpha', 'isalnum', 'isdigit', 'isspace',
509+
'isdecimal', 'isnumeric',
510+
'isidentifier', 'isprintable'):
511+
meth = getattr(str, meth_name)
512+
for s in ('\uD800', '\uDFFF', '\uD800\uD800', '\uDFFF\uDFFF',
513+
'a\uD800b\uDFFF', 'a\uDFFFb\uD800',
514+
'a\uD800b\uDFFFa', 'a\uDFFFb\uD800a'):
515+
self.assertFalse(meth(s), '%a.%s() is False' % (s, meth_name))
516+
517+
518+
@unittest.skipIf(sys.maxunicode == 65535, 'test requires wide build')
519+
def test_lower(self):
520+
string_tests.CommonTest.test_lower(self)
521+
self.assertEqual('\U00010427'.lower(), '\U0001044F')
522+
self.assertEqual('\U00010427\U00010427'.lower(),
523+
'\U0001044F\U0001044F')
524+
self.assertEqual('\U00010427\U0001044F'.lower(),
525+
'\U0001044F\U0001044F')
526+
self.assertEqual('X\U00010427x\U0001044F'.lower(),
527+
'x\U0001044Fx\U0001044F')
528+
529+
@unittest.skipIf(sys.maxunicode == 65535, 'test requires wide build')
530+
def test_upper(self):
531+
string_tests.CommonTest.test_upper(self)
532+
self.assertEqual('\U0001044F'.upper(), '\U00010427')
533+
self.assertEqual('\U0001044F\U0001044F'.upper(),
534+
'\U00010427\U00010427')
535+
self.assertEqual('\U00010427\U0001044F'.upper(),
536+
'\U00010427\U00010427')
537+
self.assertEqual('X\U00010427x\U0001044F'.upper(),
538+
'X\U00010427X\U00010427')
539+
540+
@unittest.skipIf(sys.maxunicode == 65535, 'test requires wide build')
541+
def test_capitalize(self):
542+
string_tests.CommonTest.test_capitalize(self)
543+
self.assertEqual('\U0001044F'.capitalize(), '\U00010427')
544+
self.assertEqual('\U0001044F\U0001044F'.capitalize(),
545+
'\U00010427\U0001044F')
546+
self.assertEqual('\U00010427\U0001044F'.capitalize(),
547+
'\U00010427\U0001044F')
548+
self.assertEqual('\U0001044F\U00010427'.capitalize(),
549+
'\U00010427\U0001044F')
550+
self.assertEqual('X\U00010427x\U0001044F'.capitalize(),
551+
'X\U0001044Fx\U0001044F')
552+
553+
@unittest.skipIf(sys.maxunicode == 65535, 'test requires wide build')
554+
def test_title(self):
555+
string_tests.MixinStrUnicodeUserStringTest.test_title(self)
556+
self.assertEqual('\U0001044F'.title(), '\U00010427')
557+
self.assertEqual('\U0001044F\U0001044F'.title(),
558+
'\U00010427\U0001044F')
559+
self.assertEqual('\U0001044F\U0001044F \U0001044F\U0001044F'.title(),
560+
'\U00010427\U0001044F \U00010427\U0001044F')
561+
self.assertEqual('\U00010427\U0001044F \U00010427\U0001044F'.title(),
562+
'\U00010427\U0001044F \U00010427\U0001044F')
563+
self.assertEqual('\U0001044F\U00010427 \U0001044F\U00010427'.title(),
564+
'\U00010427\U0001044F \U00010427\U0001044F')
565+
self.assertEqual('X\U00010427x\U0001044F X\U00010427x\U0001044F'.title(),
566+
'X\U0001044Fx\U0001044F X\U0001044Fx\U0001044F')
567+
568+
@unittest.skipIf(sys.maxunicode == 65535, 'test requires wide build')
569+
def test_swapcase(self):
570+
string_tests.CommonTest.test_swapcase(self)
571+
self.assertEqual('\U0001044F'.swapcase(), '\U00010427')
572+
self.assertEqual('\U00010427'.swapcase(), '\U0001044F')
573+
self.assertEqual('\U0001044F\U0001044F'.swapcase(),
574+
'\U00010427\U00010427')
575+
self.assertEqual('\U00010427\U0001044F'.swapcase(),
576+
'\U0001044F\U00010427')
577+
self.assertEqual('\U0001044F\U00010427'.swapcase(),
578+
'\U00010427\U0001044F')
579+
self.assertEqual('X\U00010427x\U0001044F'.swapcase(),
580+
'x\U0001044FX\U00010427')
581+
426582
def test_contains(self):
427583
# Testing Unicode contains method
428584
self.assertIn('a', 'abdb')

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ What's New in Python 3.2.3?
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #9200: The str.is* methods now work with strings that contain non-BMP
14+
characters even in narrow Unicode builds.
15+
1316
- Issue #12791: Break reference cycles early when a generator exits with
1417
an exception.
1518

0 commit comments

Comments
 (0)