Files
Luan Taraschi 8d0e232111 Fix re.findall reporting a group that did not match (#8563)
findall returns the matched text rather than a match object, so a group that
took no part in the match is reported as an empty value. With one group it
was reported as `None`:

    >>> re.findall(r"(a)?b", "b ab")
    [None, 'a']          # CPython: ['', 'a']

The branch for two or more groups was already right, since it passes `""` to
`Match.groups` as the default, so the two halves of the same function
disagreed with each other.

That default is built as a `str` whatever the pattern is, so a `bytes`
pattern came back with `str` mixed into it:

    >>> re.findall(rb"(a)|(b)", b"ab")
    [(b'a', ''), ('', b'b')]     # CPython: [(b'a', b''), (b'', b'b')]

The empty value is now built once from `isbytes` and both branches use it.
`Match.groups` still reports `None`, which is what CPython does.

Assisted-by: Claude Code:claude-opus-5
2026-08-21 17:13:29 +09:00

129 lines
4.6 KiB
Python

import re
haystack = "Hello world"
needle = "ello"
mo = re.search(needle, haystack)
print(mo)
# Does not work on python 3.6:
assert isinstance(mo, re.Match)
assert mo.start() == 1
assert mo.end() == 5
assert re.escape("python.exe") == "python\\.exe"
p = re.compile("ab")
s = p.sub("x", "abcabca")
# print(s)
assert s == "xcxca"
idpattern = r"([_a-z][_a-z0-9]*)"
mo = re.search(idpattern, "7382 _boe0+2")
assert mo.group(0) == "_boe0"
# tes op range
assert re.compile("[a-z]").match("a").span() == (0, 1)
assert re.compile("[a-z]").fullmatch("z").span() == (0, 1)
# test op charset
assert re.compile("[_a-z0-9]*").match("_09az").group() == "_09az"
# test op bigcharset
assert re.compile("[你好a-z]*").match("a好z你?").group() == "a好z你"
assert re.compile("[你好a-z]+").search("1232321 a好z你 !!?").group() == "a好z你"
# test op repeat one
assert re.compile("a*").match("aaa").span() == (0, 3)
assert re.compile("abcd*").match("abcdddd").group() == "abcdddd"
assert re.compile("abcd*").match("abc").group() == "abc"
assert re.compile("abcd*e").match("abce").group() == "abce"
assert re.compile("abcd*e+").match("abcddeee").group() == "abcddeee"
assert re.compile("abcd+").match("abcddd").group() == "abcddd"
# test op mark
assert re.compile("(a)b").match("ab").group(0, 1) == ("ab", "a")
assert re.compile("a(b)(cd)").match("abcd").group(0, 1, 2) == ("abcd", "b", "cd")
# test op repeat
assert re.compile("(ab)+").match("abab")
assert re.compile("(a)(b)(cd)*").match("abcdcdcd").group(0, 1, 2, 3) == (
"abcdcdcd",
"a",
"b",
"cd",
)
assert re.compile("ab()+cd").match("abcd").group() == "abcd"
assert re.compile("(a)+").match("aaa").groups() == ("a",)
assert re.compile("(a+)").match("aaa").groups() == ("aaa",)
# test Match object method
assert re.compile("(a)(bc)").match("abc")[1] == "a"
assert re.compile("a(b)(?P<a>c)d").match("abcd").groupdict() == {"a": "c"}
# test op branch
assert re.compile(r"((?=\d|\.\d)(?P<int>\d*)|a)").match("123.2132").group() == "123"
assert re.sub(r"^\s*", "X", "test") == "Xtest"
assert re.match(r"\babc\b", "abc").group() == "abc"
urlpattern = re.compile("//([^/#?]*)(.*)", re.DOTALL)
url = "//www.example.org:80/foo/bar/baz.html"
assert urlpattern.match(url).group(1) == "www.example.org:80"
assert re.compile("(?:\w+(?:\s|/(?!>))*)*").match("a /bb />ccc").group() == "a /bb "
assert re.compile("(?:(1)?)*").match("111").group() == "111"
# Test of fix re.fullmatch POSSESSIVE_REPEAT, issue #7183
assert re.fullmatch(r"([0-9]++(?:\.[0-9]+)*+)", "1.25.38")
assert re.fullmatch(r"([0-9]++(?:\.[0-9]+)*+)", "1.25.38").group(0) == "1.25.38"
# Combining characters; issue #7518
assert not re.match(r"\w", "\u0345"), r"\w should not match U+0345 (category Mn)"
def test_findall_group_that_did_not_participate():
# findall returns the matched text, not a match object, so a group that
# took no part in the match stands in as an empty value of the type the
# pattern works on. One group used to come back as None, and a bytes
# pattern used to mix str into its results.
assert re.findall(r"(a)?b", "b ab") == ["", "a"]
assert re.findall(r"(x)?", "a") == ["", ""]
assert re.findall(r"(a|b)?c", "c ac bc") == ["", "a", "b"]
assert re.findall(r"(?P<g>a)?b", "b ab") == ["", "a"]
assert re.compile(r"(a)?b").findall("b ab") == ["", "a"]
assert re.findall(rb"(a)?b", b"b ab") == [b"", b"a"]
assert re.findall(rb"(x)?", b"a") == [b"", b""]
# Two or more groups give a tuple per match, with the same stand-in.
assert re.findall(r"(a)|(b)", "ab") == [("a", ""), ("", "b")]
assert re.findall(rb"(a)|(b)", b"ab") == [(b"a", b""), (b"", b"b")]
assert re.findall(rb"(a)(b)?", b"a ab") == [(b"a", b""), (b"a", b"b")]
# The type is the pattern's, never the other one.
assert [type(x) for x in re.findall(r"(a)?b", "b ab")] == [str, str]
assert [type(x) for x in re.findall(rb"(a)?b", b"b ab")] == [bytes, bytes]
assert [type(y) for x in re.findall(rb"(a)|(b)", b"ab") for y in x] == [
bytes,
bytes,
bytes,
bytes,
]
# A group that does participate, and no group at all, are unchanged.
assert re.findall(r"(a)", "aa") == ["a", "a"]
assert re.findall(r"a", "aa") == ["a", "a"]
assert re.findall(rb"a", b"aa") == [b"a", b"a"]
# A match object still reports None, which is where the difference lies.
assert re.match(r"(a)?b", "b").groups() == (None,)
assert re.match(r"(a)?b", "b").group(1) is None
assert [m.groups() for m in re.finditer(r"(a)?b", "b ab")] == [(None,), ("a",)]
assert re.split(r"(a)|(b)", "xaybz") == ["x", "a", None, "y", None, "b", "z"]
test_findall_group_that_did_not_participate()