Skip to content

Commit 6079ad0

Browse files
authored
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
1 parent c077373 commit 6079ad0

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎crates/vm/src/stdlib/_sre.rs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -452,15 +452,25 @@ mod _sre {
452452
let mut match_list: Vec<PyObjectRef> = Vec::new();
453453
let mut iter = SearchIter { req, state };
454454

455+
// What a group that took no part in the match is reported as.
456+
// `findall` hands back the matched text rather than a match
457+
// object, so the stand-in has to be an empty value of the type
458+
// the pattern works on. `Match.groups` still reports `None` and
459+
// is not affected by this.
460+
let empty: PyObjectRef = if zelf.isbytes {
461+
vm.ctx.new_bytes(vec![]).into()
462+
} else {
463+
vm.ctx.new_str(ascii!("")).into()
464+
};
465+
455466
while iter.next().is_some() {
456467
let m = Match::new(&mut iter.state, zelf.clone(), string_args.string.clone());
457468

458469
let item = if zelf.groups == 0 || zelf.groups == 1 {
459470
m.get_slice(zelf.groups, s, vm)
460-
.unwrap_or_else(|| vm.ctx.none())
471+
.unwrap_or_else(|| empty.clone())
461472
} else {
462-
m.groups(OptionalArg::Present(vm.ctx.new_str(ascii!("")).into()), vm)?
463-
.into()
473+
m.groups(OptionalArg::Present(empty.clone()), vm)?.into()
464474
};
465475

466476
match_list.push(item);

‎extra_tests/snippets/stdlib_re.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,47 @@
8282

8383
# Combining characters; issue #7518
8484
assert not re.match(r"\w", "\u0345"), r"\w should not match U+0345 (category Mn)"
85+
86+
87+
def test_findall_group_that_did_not_participate():
88+
# findall returns the matched text, not a match object, so a group that
89+
# took no part in the match stands in as an empty value of the type the
90+
# pattern works on. One group used to come back as None, and a bytes
91+
# pattern used to mix str into its results.
92+
assert re.findall(r"(a)?b", "b ab") == ["", "a"]
93+
assert re.findall(r"(x)?", "a") == ["", ""]
94+
assert re.findall(r"(a|b)?c", "c ac bc") == ["", "a", "b"]
95+
assert re.findall(r"(?P<g>a)?b", "b ab") == ["", "a"]
96+
assert re.compile(r"(a)?b").findall("b ab") == ["", "a"]
97+
98+
assert re.findall(rb"(a)?b", b"b ab") == [b"", b"a"]
99+
assert re.findall(rb"(x)?", b"a") == [b"", b""]
100+
101+
# Two or more groups give a tuple per match, with the same stand-in.
102+
assert re.findall(r"(a)|(b)", "ab") == [("a", ""), ("", "b")]
103+
assert re.findall(rb"(a)|(b)", b"ab") == [(b"a", b""), (b"", b"b")]
104+
assert re.findall(rb"(a)(b)?", b"a ab") == [(b"a", b""), (b"a", b"b")]
105+
106+
# The type is the pattern's, never the other one.
107+
assert [type(x) for x in re.findall(r"(a)?b", "b ab")] == [str, str]
108+
assert [type(x) for x in re.findall(rb"(a)?b", b"b ab")] == [bytes, bytes]
109+
assert [type(y) for x in re.findall(rb"(a)|(b)", b"ab") for y in x] == [
110+
bytes,
111+
bytes,
112+
bytes,
113+
bytes,
114+
]
115+
116+
# A group that does participate, and no group at all, are unchanged.
117+
assert re.findall(r"(a)", "aa") == ["a", "a"]
118+
assert re.findall(r"a", "aa") == ["a", "a"]
119+
assert re.findall(rb"a", b"aa") == [b"a", b"a"]
120+
121+
# A match object still reports None, which is where the difference lies.
122+
assert re.match(r"(a)?b", "b").groups() == (None,)
123+
assert re.match(r"(a)?b", "b").group(1) is None
124+
assert [m.groups() for m in re.finditer(r"(a)?b", "b ab")] == [(None,), ("a",)]
125+
assert re.split(r"(a)|(b)", "xaybz") == ["x", "a", None, "y", None, "b", "z"]
126+
127+
128+
test_findall_group_that_did_not_participate()

0 commit comments

Comments
 (0)