Skip to content

Commit f265305

Browse files
gh-152026: Support redefinition of named capture groups
The same name can now be used for more than one capturing group. All such groups share a single group number; the name and number refer to whichever of the groups matched (the last one, if more than one matched). The group's width range spans all the definitions. This is chiefly useful for giving the same name to corresponding groups in alternative spellings of a pattern: (?P<m>\d+)/(?P<d>\d+)/(?P<y>\d+)|(?P<y>\d+)-(?P<m>\d+)-(?P<d>\d+) Previously each group name had to be unique within a regular expression. Sharing a group number also requires a fix in the matching engine. Outside of repeats the matcher restores capture-group marks lazily, only rewinding the lastmark high-water index, which assumes each group number is written at a single place in the bytecode. A reused group number could otherwise leak a mark from a branch that matched and was then backtracked away, or raise SystemError. Reused group numbers are detected during code validation, and such patterns save and restore the full mark array on backtracking, exactly as is already done inside repeats; other patterns are unaffected. The same flag also lets the possessive quantifier handler request mark saving directly, replacing the placeholder repeat context that was previously installed for that purpose (the gh-101955 workaround), so a possessive repeat no longer allocates a repeat context. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8b1dbb1 commit f265305

8 files changed

Lines changed: 168 additions & 59 deletions

File tree

Doc/library/re.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,9 +436,15 @@ The special characters are:
436436
Similar to regular parentheses, but the substring matched by the group is
437437
accessible via the symbolic group name *name*. Group names must be valid
438438
Python identifiers, and in :class:`bytes` patterns they can only contain
439-
bytes in the ASCII range. Each group name must be defined only once within
440-
a regular expression. A symbolic group is also a numbered group, just as if
441-
the group were not named.
439+
bytes in the ASCII range. A symbolic group is also a numbered group, just as
440+
if the group were not named.
441+
442+
A group name may be used for more than one group. All such groups share a
443+
single group number, and the name (and that number) refer to whichever of
444+
them matched; if more than one matched, they refer to the last. This is
445+
chiefly useful for giving the same name to corresponding groups in
446+
alternative spellings of a pattern, for example
447+
``(?P<y>\d{4})-(?P<m>\d\d)|(?P<m>\d\d)/(?P<y>\d{4})``.
442448

443449
Named groups can be referenced in three contexts. If the pattern is
444450
``(?P<quote>['"]).*?(?P=quote)`` (i.e. matching a string quoted with either
@@ -462,6 +468,10 @@ The special characters are:
462468
In :class:`bytes` patterns, group *name* can only contain bytes
463469
in the ASCII range (``b'\x00'``-``b'\x7f'``).
464470

471+
.. versionchanged:: next
472+
A group name can be used for more than one group. Previously each name
473+
could be defined only once in a regular expression.
474+
465475
.. index:: single: (?P=; in regular expressions
466476

467477
``(?P=name)``

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ re
288288
properties.
289289
(Contributed by Serhiy Storchaka in :gh:`95555`.)
290290

291+
* A capturing group name can now be used for more than one group in a regular
292+
expression. Such groups share a single group number, and the name refers to
293+
whichever of them matched. This is useful for giving the same name to
294+
corresponding groups in alternative spellings of a pattern, such as
295+
``(?P<y>\d{4})-(?P<m>\d\d)|(?P<m>\d\d)/(?P<y>\d{4})``.
296+
(Contributed by Serhiy Storchaka in :gh:`152026`.)
297+
291298

292299
shlex
293300
-----

Lib/re/_parser.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,26 @@ def groups(self):
8686
return len(self.groupwidths)
8787
def opengroup(self, name=None):
8888
gid = self.groups
89-
self.groupwidths.append(None)
9089
if self.groups > MAXGROUPS:
9190
raise error("too many groups")
9291
if name is not None:
9392
ogid = self.groupdict.get(name, None)
9493
if ogid is not None:
95-
raise error("redefinition of group name %r as group %d; "
96-
"was group %d" % (name, gid, ogid))
94+
# The same name may be used for more than one group. All such
95+
# groups share a single group number, and the name refers to
96+
# whichever of them matched.
97+
return ogid
9798
self.groupdict[name] = gid
99+
self.groupwidths.append(None)
98100
return gid
99101
def closegroup(self, gid, p):
100-
self.groupwidths[gid] = p.getwidth()
102+
# A reused group number may be closed more than once; its width spans
103+
# the union of all the definitions.
104+
w = p.getwidth()
105+
wold = self.groupwidths[gid]
106+
if wold is not None:
107+
w = (min(wold[0], w[0]), max(wold[1], w[1]))
108+
self.groupwidths[gid] = w
101109
def checkgroup(self, gid):
102110
return gid < self.groups and self.groupwidths[gid] is not None
103111

Lib/test/test_re.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,6 @@ def test_symbolic_groups(self):
297297
self.assertEqual(re.match(pat, 'xc8yz').span(), (0, 5))
298298

299299
def test_symbolic_groups_errors(self):
300-
self.checkPatternError(r'(?P<a>)(?P<a>)',
301-
"redefinition of group name 'a' as group 2; "
302-
"was group 1")
303300
self.checkPatternError(r'(?P<a>(?P=a))',
304301
"cannot refer to an open group", 10)
305302
self.checkPatternError(r'(?Pxy)', 'unknown extension ?Px')
@@ -380,6 +377,69 @@ def test_symbolic_refs_errors(self):
380377
self.checkTemplateError('(?P<a>x)', r'\g<१>', 'xx',
381378
"bad character in group name '१'", 3)
382379

380+
def test_redefined_named_groups(self):
381+
# The same name may be used for more than one group.
382+
p = re.compile(r'(?P<m>\d+)/(?P<d>\d+)/(?P<y>\d+)|'
383+
r'(?P<y>\d+)-(?P<m>\d+)-(?P<d>\d+)')
384+
self.assertEqual(p.fullmatch('07/09/2023').groupdict(),
385+
{'m': '07', 'd': '09', 'y': '2023'})
386+
self.assertEqual(p.fullmatch('2023-07-09').groupdict(),
387+
{'m': '07', 'd': '09', 'y': '2023'})
388+
# Reused groups share a single group number.
389+
self.assertEqual(p.groups, 3)
390+
self.assertEqual(p.groupindex, {'m': 1, 'd': 2, 'y': 3})
391+
# If more than one of the groups matches, the name refers to the last.
392+
p = re.compile(r'(?P<a>\w)(?P<a>\w)')
393+
self.assertEqual(p.match('xy').group('a'), 'y')
394+
# A reused group may be left unset by the branch that does not match.
395+
p = re.compile(r'(?P<a>\d)(?:-(?P<a>\d))?')
396+
self.assertEqual(p.match('1').group('a'), '1')
397+
self.assertEqual(p.match('1-2').group('a'), '2')
398+
# A definition that is backtracked away does not leak into the group.
399+
p = re.compile(r'(?P<g>x)(?:(?P<g>y)|z)')
400+
self.assertEqual(p.match('xz').group('g'), 'x')
401+
self.assertEqual(p.match('xy').group('g'), 'y')
402+
403+
def test_redefined_named_groups_backref(self):
404+
# A backreference to a redefined name refers to whichever definition
405+
# participated in the match.
406+
p = re.compile(r'(?:(?P<g>a)|(?P<g>b))(?P=g)')
407+
self.assertEqual(p.match('aa').group(), 'aa')
408+
self.assertEqual(p.match('bb').group(), 'bb')
409+
self.assertIsNone(p.match('ab'))
410+
# A numeric backreference to the shared group number works too.
411+
p = re.compile(r'(?:(?P<g>a)|(?P<g>b))\1')
412+
self.assertEqual(p.match('aa').group(), 'aa')
413+
self.assertIsNone(p.match('ab'))
414+
415+
def test_redefined_named_groups_conditional(self):
416+
p = re.compile(r'(?:(?P<g>a)|b)(?(g)X|Y)')
417+
self.assertEqual(p.match('aX').group(), 'aX')
418+
self.assertEqual(p.match('bY').group(), 'bY')
419+
self.assertIsNone(p.match('aY'))
420+
self.assertIsNone(p.match('bX'))
421+
422+
def test_redefined_named_groups_width(self):
423+
# closegroup() widens the shared group to the union of the definitions.
424+
p = re.compile(r'(?:(?P<g>a)|(?P<g>bb))!')
425+
self.assertEqual(p.match('a!').group('g'), 'a')
426+
self.assertEqual(p.match('bb!').group('g'), 'bb')
427+
# A fixed-width union may be used in a look-behind...
428+
p = re.compile(r'(?:(?P<g>aa)|(?P<g>bb))(?<=(?P=g))')
429+
self.assertEqual(p.match('aa').group(), 'aa')
430+
# ...but a variable-width union may not.
431+
self.checkPatternError(r'x(?<=(?:(?P<g>a)|(?P<g>bb)))',
432+
'look-behind requires fixed-width pattern')
433+
434+
def test_redefined_named_groups_backtracking(self):
435+
# A definition that matches and is then backtracked away (here the
436+
# other branch of the inner alternation is taken) must not leak a stale
437+
# capture into the shared group number.
438+
p = re.compile(r'(?:(?P<g>a)|(?P<h>(?P<g>b)(?:b|a)|(?:bbb)*))')
439+
self.assertIsNone(p.match('b').group('g'))
440+
self.assertEqual(p.match('a').group('g'), 'a')
441+
self.assertEqual(p.match('bb').group('g'), 'b')
442+
383443
def test_re_subn(self):
384444
self.assertEqual(re.subn("(?i)b+", "x", "bbbb BBBB"), ('x x', 2))
385445
self.assertEqual(re.subn("b+", "x", "bbbb BBBB"), ('x BBBB', 1))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
A group name can now be used for more than one group in a regular expression.
2+
All such groups share a single group number, and the name refers to whichever
3+
of them matched (the last one, if more than one matched). This is chiefly
4+
useful for giving the same name to corresponding groups in alternative
5+
spellings of a pattern, e.g. ``(?P<y>\d{4})-(?P<m>\d\d)|(?P<m>\d\d)/(?P<y>\d{4})``.

Modules/_sre/sre.c

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ state_init(SRE_STATE* state, PatternObject* pattern, PyObject* string,
738738
state->charsize = charsize;
739739
state->match_all = 0;
740740
state->must_advance = 0;
741+
state->save_marks = pattern->reused_groups;
741742
state->debug = ((pattern->flags & SRE_FLAG_DEBUG) != 0);
742743

743744
state->beginning = ptr;
@@ -1791,6 +1792,7 @@ _sre_compile_impl(PyObject *module, PyObject *pattern, int flags,
17911792
self->pattern = NULL;
17921793
self->groupindex = NULL;
17931794
self->indexgroup = NULL;
1795+
self->reused_groups = 0;
17941796
#ifdef Py_DEBUG
17951797
self->fail_after_count = -1;
17961798
self->fail_after_exc = NULL;
@@ -2132,7 +2134,8 @@ _validate_charset(SRE_CODE *code, SRE_CODE *end)
21322134

21332135
/* Returns 0 on success, -1 on failure, and 1 if the last op is JUMP. */
21342136
static int
2135-
_validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
2137+
_validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups,
2138+
char *seen, int *reused)
21362139
{
21372140
/* Some variables are manipulated by the macros above */
21382141
SRE_CODE op;
@@ -2157,6 +2160,14 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
21572160
VTRACE(("arg=%d, groups=%d\n", (int)arg, (int)groups));
21582161
FAIL;
21592162
}
2163+
/* A mark index appears exactly once in well-formed code, unless
2164+
the same group number is opened in more than one place (a
2165+
redefined named group). Such groups need full mark save/restore
2166+
on backtracking. */
2167+
if (seen[arg])
2168+
*reused = 1;
2169+
else
2170+
seen[arg] = 1;
21602171
break;
21612172

21622173
case SRE_OP_LITERAL:
@@ -2290,7 +2301,7 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
22902301
if (skip == 0)
22912302
break;
22922303
/* Stop 2 before the end; we check the JUMP below */
2293-
if (_validate_inner(code, code+skip-3, groups))
2304+
if (_validate_inner(code, code+skip-3, groups, seen, reused))
22942305
FAIL;
22952306
code += skip-3;
22962307
/* Check that it ends with a JUMP, and that each JUMP
@@ -2321,7 +2332,7 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
23212332
FAIL;
23222333
if (max > SRE_MAXREPEAT)
23232334
FAIL;
2324-
if (_validate_inner(code, code+skip-4, groups))
2335+
if (_validate_inner(code, code+skip-4, groups, seen, reused))
23252336
FAIL;
23262337
code += skip-4;
23272338
GET_OP;
@@ -2341,7 +2352,7 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
23412352
FAIL;
23422353
if (max > SRE_MAXREPEAT)
23432354
FAIL;
2344-
if (_validate_inner(code, code+skip-3, groups))
2355+
if (_validate_inner(code, code+skip-3, groups, seen, reused))
23452356
FAIL;
23462357
code += skip-3;
23472358
GET_OP;
@@ -2359,7 +2370,7 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
23592370
case SRE_OP_ATOMIC_GROUP:
23602371
{
23612372
GET_SKIP;
2362-
if (_validate_inner(code, code+skip-2, groups))
2373+
if (_validate_inner(code, code+skip-2, groups, seen, reused))
23632374
FAIL;
23642375
code += skip-2;
23652376
GET_OP;
@@ -2412,12 +2423,12 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
24122423
for a JUMP opcode preceding our skip target.
24132424
*/
24142425
VTRACE(("then part:\n"));
2415-
int rc = _validate_inner(code+1, code+skip-1, groups);
2426+
int rc = _validate_inner(code+1, code+skip-1, groups, seen, reused);
24162427
if (rc == 1) {
24172428
VTRACE(("else part:\n"));
24182429
code += skip-2; /* Position after JUMP, at <skipno> */
24192430
GET_SKIP;
2420-
rc = _validate_inner(code, code+skip-1, groups);
2431+
rc = _validate_inner(code, code+skip-1, groups, seen, reused);
24212432
}
24222433
if (rc)
24232434
FAIL;
@@ -2430,7 +2441,7 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
24302441
GET_ARG; /* 0 for lookahead, width for lookbehind */
24312442
code--; /* Back up over arg to simplify math below */
24322443
/* Stop 1 before the end; we check the SUCCESS below */
2433-
if (_validate_inner(code+1, code+skip-2, groups))
2444+
if (_validate_inner(code+1, code+skip-2, groups, seen, reused))
24342445
FAIL;
24352446
code += skip-2;
24362447
GET_OP;
@@ -2455,24 +2466,35 @@ _validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
24552466
}
24562467

24572468
static int
2458-
_validate_outer(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
2469+
_validate_outer(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups,
2470+
char *seen, int *reused)
24592471
{
24602472
if (groups < 0 || (size_t)groups > SRE_MAXGROUPS ||
24612473
code >= end || end[-1] != SRE_OP_SUCCESS)
24622474
FAIL;
2463-
return _validate_inner(code, end-1, groups);
2475+
return _validate_inner(code, end-1, groups, seen, reused);
24642476
}
24652477

24662478
static int
24672479
_validate(PatternObject *self)
24682480
{
2469-
if (_validate_outer(self->code, self->code+self->codesize, self->groups))
2470-
{
2481+
/* seen[i] tracks whether mark index i has already been emitted, so that a
2482+
reused group number (the same mark seen twice) can be detected. */
2483+
int reused = 0;
2484+
char *seen = PyMem_Calloc(2 * (size_t)self->groups + 1, 1);
2485+
if (seen == NULL) {
2486+
PyErr_NoMemory();
2487+
return 0;
2488+
}
2489+
int invalid = _validate_outer(self->code, self->code+self->codesize,
2490+
self->groups, seen, &reused);
2491+
PyMem_Free(seen);
2492+
if (invalid) {
24712493
PyErr_SetString(PyExc_RuntimeError, "invalid SRE code");
24722494
return 0;
24732495
}
2474-
else
2475-
VTRACE(("Success!\n"));
2496+
self->reused_groups = reused;
2497+
VTRACE(("Success!\n"));
24762498
return 1;
24772499
}
24782500

Modules/_sre/sre.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ typedef struct {
3434
int flags; /* flags used when compiling pattern source */
3535
PyObject *weakreflist; /* List of weak references */
3636
int isbytes; /* pattern type (1 - bytes, 0 - string, -1 - None) */
37+
int reused_groups; /* a group number is opened in more than one place
38+
(a redefined named group) */
3739
#ifdef Py_DEBUG
3840
/* for simulation of user interruption */
3941
int fail_after_count;
@@ -97,6 +99,11 @@ typedef struct {
9799
int lastmark;
98100
int lastindex;
99101
const void** mark;
102+
int save_marks; /* if nonzero, save and restore mark values on
103+
backtracking instead of only rewinding the lastmark
104+
index; needed when a group inside the current region can
105+
be revisited (reused group numbers, or the body of a
106+
possessive repeat) */
100107
/* dynamically allocated stuff */
101108
char* data_stack;
102109
size_t data_stack_size;

0 commit comments

Comments
 (0)