mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
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
This commit is contained in:
1 parent
c0773738d8
commit
6079ad06e5
2 files changed
+57
-3
No files matched your search
@@ -452,15 +452,25 @@ mod _sre {
|
||||
let mut match_list: Vec<PyObjectRef> = Vec::new();
|
||||
let mut iter = SearchIter { req, state };
|
||||
|
||||
// What a group that took no part in the match is reported as.
|
||||
// `findall` hands back the matched text rather than a match
|
||||
// object, so the stand-in has to be an empty value of the type
|
||||
// the pattern works on. `Match.groups` still reports `None` and
|
||||
// is not affected by this.
|
||||
let empty: PyObjectRef = if zelf.isbytes {
|
||||
vm.ctx.new_bytes(vec![]).into()
|
||||
} else {
|
||||
vm.ctx.new_str(ascii!("")).into()
|
||||
};
|
||||
|
||||
while iter.next().is_some() {
|
||||
let m = Match::new(&mut iter.state, zelf.clone(), string_args.string.clone());
|
||||
|
||||
let item = if zelf.groups == 0 || zelf.groups == 1 {
|
||||
m.get_slice(zelf.groups, s, vm)
|
||||
.unwrap_or_else(|| vm.ctx.none())
|
||||
.unwrap_or_else(|| empty.clone())
|
||||
} else {
|
||||
m.groups(OptionalArg::Present(vm.ctx.new_str(ascii!("")).into()), vm)?
|
||||
.into()
|
||||
m.groups(OptionalArg::Present(empty.clone()), vm)?.into()
|
||||
};
|
||||
|
||||
match_list.push(item);
|
||||
|
||||
@@ -82,3 +82,47 @@ 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()
|
||||
Reference in new issue
Block a user