Skip to content

Commit c8188a5

Browse files
committed
bpo-18236: Adjust str.isspace to use Unicode's White_Space property.
When Unicode support was first added to Python, there was no Unicode property identifying whitespace, so we approximated it by putting together a couple of other properties. Now there is a White_Space property, so let's use it. Happily, the difference from our original approximation is only in the four rare control characters 001C..001F. As a bonus, `isspace` now joins all similar methods in giving exactly matching results for ASCII characters represented as `str` or as `bytes`. Add a test for that nice property.
1 parent 272d0d0 commit c8188a5

8 files changed

Lines changed: 1706 additions & 1677 deletions

File tree

‎Doc/library/stdtypes.rst‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,10 +1764,13 @@ expression support in the :mod:`re` module).
17641764
Return true if there are only whitespace characters in the string and there is
17651765
at least one character, false otherwise.
17661766

1767-
A character is *whitespace* if in the Unicode character database
1768-
(see :mod:`unicodedata`), either its general category is ``Zs``
1769-
("Separator, space"), or its bidirectional class is one of ``WS``,
1770-
``B``, or ``S``.
1767+
The whitespace characters are `as defined by Unicode
1768+
<https://www.unicode.org/reports/tr44/#White_Space>`_.
1769+
1770+
.. versionchanged:: 3.9
1771+
Previously used a different definition of whitespace characters,
1772+
which included the additional characters ``\x1c``, ``\x1d``,
1773+
``\x1e``, and ``\x1f``.
17711774

17721775

17731776
.. method:: str.istitle()

‎Lib/test/string_tests.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,7 @@ def test_isspace(self):
884884
self.checkequal(True, '\t', 'isspace')
885885
self.checkequal(True, '\r', 'isspace')
886886
self.checkequal(True, '\n', 'isspace')
887+
self.checkequal(False, '\x1c', 'isspace')
887888
self.checkequal(True, ' \t\r\n', 'isspace')
888889
self.checkequal(False, ' \t\r\na', 'isspace')
889890
self.checkraises(TypeError, 'abc', 'isspace', 42)

‎Lib/test/test_unicode.py‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,13 +625,21 @@ def test_isspace(self):
625625

626626
@support.requires_resource('cpu')
627627
def test_isspace_invariant(self):
628+
'''
629+
Before Python 3.9, `str.isspace` used a certain formula
630+
combining the General Category and the Bidirectional Class.
631+
632+
Since Python 3.9, we use the White_Space property that Unicode
633+
now provides. Check that these differ only on U+001C..U+001F .
634+
'''
628635
for codepoint in range(sys.maxunicode + 1):
629636
char = chr(codepoint)
630637
bidirectional = unicodedata.bidirectional(char)
631638
category = unicodedata.category(char)
632639
self.assertEqual(char.isspace(),
633-
(bidirectional in ('WS', 'B', 'S')
634-
or category == 'Zs'))
640+
((bidirectional in ('WS', 'B', 'S')
641+
or category == 'Zs')
642+
and codepoint not in range(0x1c, 0x20)))
635643

636644
def test_isalnum(self):
637645
super().test_isalnum()
@@ -705,6 +713,21 @@ def test_isnumeric(self):
705713
'\U000104A0', '\U0001F107']:
706714
self.assertTrue(ch.isnumeric(), '{!a} is numeric.'.format(ch))
707715

716+
def test_bytes_consistency(self):
717+
'''Check str and bytes agree on character properties within ASCII.'''
718+
for codepoint in range(0x80):
719+
char = chr(codepoint)
720+
byte = bytes([codepoint])
721+
# Including the codepoint in the tuple gets it printed on failure.
722+
self.assertEqual(
723+
(codepoint, char.islower(), char.isupper(), char.isalpha(),
724+
char.isdigit(), char.isalnum(), char.isspace()),
725+
(codepoint, byte.islower(), byte.isupper(), byte.isalpha(),
726+
byte.isdigit(), byte.isalnum(), byte.isspace()))
727+
self.assertEqual(
728+
(codepoint, ord(char.lower()), ord(char.upper())),
729+
(codepoint, ord(byte.lower()), ord(byte.upper())))
730+
708731
def test_isidentifier(self):
709732
self.assertTrue("a".isidentifier())
710733
self.assertTrue("Z".isidentifier())

‎Lib/test/test_unicodedata.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
class UnicodeMethodsTest(unittest.TestCase):
1818

1919
# update this, if the database changes
20-
expectedchecksum = 'e728278035eb76cf92d86f07852266b0433f16a5'
20+
expectedchecksum = 'd0652da04440a4ea690fbd6fd3bf79af39090547'
2121

2222
@requires_resource('cpu')
2323
def test_method_checksum(self):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The definition of "whitespace" provided by :meth:`str.isspace` and
2+
:c:func:`Py_UNICODE_ISSPACE` now follows Unicode's ``White_Space`` property.
3+
This has the effect that the four characters ``\x1c``, ``\x1d``, ``\x1e``,
4+
``\x1f`` are no longer treated as whitespace. Patch by Greg Price.

‎Objects/unicodeobject.c‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,7 @@ const unsigned char _Py_ascii_whitespace[] = {
292292
/* case 0x000D: * CARRIAGE RETURN */
293293
0, 1, 1, 1, 1, 1, 0, 0,
294294
0, 0, 0, 0, 0, 0, 0, 0,
295-
/* case 0x001C: * FILE SEPARATOR */
296-
/* case 0x001D: * GROUP SEPARATOR */
297-
/* case 0x001E: * RECORD SEPARATOR */
298-
/* case 0x001F: * UNIT SEPARATOR */
299-
0, 0, 0, 0, 1, 1, 1, 1,
295+
0, 0, 0, 0, 0, 0, 0, 0,
300296
/* case 0x0020: * SPACE */
301297
1, 0, 0, 0, 0, 0, 0, 0,
302298
0, 0, 0, 0, 0, 0, 0, 0,

‎Objects/unicodetype_db.h‎

Lines changed: 1658 additions & 1661 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Tools/unicode/makeunicodedata.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#
2828

2929
import dataclasses
30+
import itertools
3031
import os
3132
import sys
3233
import zipfile
@@ -46,6 +47,7 @@
4647
# * Doc/reference/lexical_analysis.rst (two occurrences)
4748
UNIDATA_VERSION = "12.1.0"
4849
UNICODE_DATA = "UnicodeData%s.txt"
50+
PROP_LIST = "PropList%s.txt"
4951
COMPOSITION_EXCLUSIONS = "CompositionExclusions%s.txt"
5052
EASTASIAN_WIDTH = "EastAsianWidth%s.txt"
5153
UNIHAN = "Unihan%s.zip"
@@ -416,7 +418,7 @@ def makeunicodetype(unicode, trace):
416418
if 'Line_Break' in properties or bidirectional == "B":
417419
flags |= LINEBREAK_MASK
418420
linebreaks.append(char)
419-
if category == "Zs" or bidirectional in ("WS", "B", "S"):
421+
if 'White_Space' in properties:
420422
flags |= SPACE_MASK
421423
spaces.append(char)
422424
if category == "Lt":
@@ -561,8 +563,7 @@ def makeunicodetype(unicode, trace):
561563
fprint()
562564

563565
# Generate code for _PyUnicode_IsWhitespace()
564-
fprint("/* Returns 1 for Unicode characters having the bidirectional")
565-
fprint(" * type 'WS', 'B' or 'S' or the category 'Zs', 0 otherwise.")
566+
fprint("/* Returns 1 for Unicode characters with property White_Space.")
566567
fprint(" */")
567568
fprint('int _PyUnicode_IsWhitespace(const Py_UCS4 ch)')
568569
fprint('{')
@@ -974,6 +975,7 @@ class UcdRecord:
974975

975976
# Binary properties, as a set of those that are true.
976977
# Taken from multiple files:
978+
# https://www.unicode.org/reports/tr44/#PropList.txt
977979
# https://www.unicode.org/reports/tr44/#DerivedCoreProperties.txt
978980
# https://www.unicode.org/reports/tr44/#LineBreak.txt
979981
binary_properties: Set[str]
@@ -1081,7 +1083,10 @@ def __init__(self, version, cjk_check=True):
10811083
if table[i] is not None:
10821084
table[i].east_asian_width = widths[i]
10831085

1084-
for char, (p,) in UcdFile(DERIVED_CORE_PROPERTIES, version).expanded():
1086+
for char, (p,) in itertools.chain(
1087+
UcdFile(PROP_LIST, version).expanded(),
1088+
UcdFile(DERIVED_CORE_PROPERTIES, version).expanded(),
1089+
):
10851090
if table[char]:
10861091
# Some properties (e.g. Default_Ignorable_Code_Point)
10871092
# apply to unassigned code points; ignore them

0 commit comments

Comments
 (0)