8000 gh-105636: Add re.Pattern.compile_template() · python/cpython@25653db · GitHub
[go: up one dir, main page]

Skip to content

Commit 25653db

Browse files
gh-105636: Add re.Pattern.compile_template()
1 parent 6be17ba commit 25653db

File tree

7 files changed

+246
-16
lines changed

7 files changed

+246
-16
lines changed

Doc/library/re.rst

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,9 @@ Functions
10641064

10651065
Return the string obtained by replacing the leftmost non-overlapping occurrences
10661066
of *pattern* in *string* by the replacement *repl*. If the pattern isn't found,
1067-
*string* is returned unchanged. *repl* can be a string or a function; if it is
1067+
*string* is returned unchanged.
1068+
*repl* can be a string, a :ref:`template object <template-objects>`,
1069+
or a callable; if it is
10681070
a string, any backslash escapes in it are processed. That is, ``\n`` is
10691071
converted to a single newline character, ``\r`` is converted to a carriage return, and
10701072
so forth. Unknown escapes of ASCII letters are reserved for future use and
@@ -1093,6 +1095,13 @@ Functions
10931095

10941096
The pattern may be a string or a :class:`~re.Pattern`.
10951097

1098+
The replacement string can be compiled as well as the pattern::
1099+
1100+
>>> pat = re.compile(r'def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):')
1101+
>>> repl = pat.compile_template(r'static PyObject*\npy_\1(void)\n{')
1102+
>>> re.sub(pat, repl, 'def myfunc():')
1103+
'static PyObject*\npy_myfunc(void)\n{'
1104+
10961105
The optional argument *count* is the maximum number of pattern occurrences to be
10971106
replaced; *count* must be a non-negative integer. If omitted or zero, all
10981107
occurrences will be replaced.
@@ -1143,6 +1152,9 @@ Functions
11431152
In future Python versions they will be
11441153
:ref:`keyword-only parameters <keyword-only_parameter>`.
11451154

1155+
.. versionchanged:: next
1156+
*repl* can be compiled.
1157+
11461158

11471159
.. function:: subn(pattern, repl, string, count=0, flags=0)
11481160

@@ -1337,6 +1349,16 @@ Regular Expression Objects
13371349
Identical to the :func:`subn` function, using the compiled pattern.
13381350

13391351

1352+
.. method:: Pattern.compile_template(repl)
1353+
1354+
Compile a replacement string into a :ref:`template object
1355+
<template-objects>`, which can be used for replacing patterns in strings
1356+
using functions :re:`re.sub` or :re:`re.subn` or corresponding methods of
1357+
the :ref:`pattern object <re-objects>`.
1358+
1359+
.. versionadded:: next
1360+
1361+
13401362
.. attribute:: Pattern.flags
13411363

13421364
The regex matching flags. This is a combination of the flags given to
@@ -1586,6 +1608,25 @@ when there is no match, you can test whether there was a match with a simple
15861608
are considered atomic.
15871609

15881610

1611+
.. _template-objects:
1612+
1613+
Template Objects
1614+
----------------
1615+
1616+
6D40 A replacement string can be compiled into a template object using the :meth:`~re.Pattern.compile_template` method.
1617+
1618+
.. versionadded:: next
1619+
1620+
Template object is a callable which takes a single :ref:`match object
1621+
<match-objects>` argument, and returns the replacement string with group
1622+
references resolved.
1623+
1624+
>>> pat = re.compile('(.)(.)')
1625+
>>> templ = pat.compile_template(r'\2-\1')
1626+
>>> templ(pat.match('ab'))
1627+
'b-a'
1628+
1629+
15891630
.. _re-examples:
15901631

15911632
Regular Expression Examples

Doc/whatsnew/3.15.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ os.path
126126
(Contributed by Petr Viktorin for :cve:`2025-4517`.)
127127

128128

129+
re
130+
--
131+
132+
* Add the :meth:`~re.Pattern.compile_template` method for the
133+
:ref:`pattern object <re-objects>` which allows to pre-compile
134+
replacement strings.
135+
(Contributed by Serhiy Storchaka in :gh:`105636`.)
136+
137+
129138
shelve
130139
------
131140

Lib/re/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
__all__ = [
133133
"match", "fullmatch", "search", "sub", "subn", "split",
134134
"findall", "finditer", "compile", "purge", "escape",
135-
"error", "Pattern", "Match", "A", "I", "L", "M", "S", "X", "U",
135+
"error", "Pattern", "Match", "Template", "A", "I", "L", "M", "S", "X", "U",
136136
"ASCII", "IGNORECASE", "LOCALE", "MULTILINE", "DOTALL", "VERBOSE",
137137
"UNICODE", "NOFLAG", "RegexFlag", "PatternError"
138138
]
@@ -312,8 +312,12 @@ def escape(pattern):
312312
pattern = str(pattern, 'latin1')
313313
return pattern.translate(_special_chars_map).encode('latin1')
314314

315-
Pattern = type(_compiler.compile('', 0))
316-
Match = type(_compiler.compile('', 0).match(''))
315+
p = _compiler.compile('', 0)
316+
Pattern = type(p)
317+
Match = type(p.match(''))
318+
import _sre
319+
Template = type(_sre.template(p, ['']))
320+
del p
317321

318322
# --------------------------------------------------------------------
319323
# internals
@@ -374,6 +378,8 @@ def _compile(pattern, flags):
374378
@functools.lru_cache(_MAXCACHE)
375379
def _compile_template(pattern, repl):
376380
# internal: compile replacement pattern
381+
if isinstance(repl, Template):
382+
return repr
377383
return _sre.template(pattern, _parser.parse_template(repl, pattern))
378384

379385
# register myself for pickling

Lib/test/test_re.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2889,6 +2889,79 @@ def test_flags_repr(self):
28892889
"re.ASCII|re.LOCALE|re.UNICODE|re.MULTILINE|re.DEBUG|0xffe01")
28902890

28912891

2892+
class TemplateTests(unittest.TestCase):
2893+
def test_literal(self):
2894+
p = re.compile(r'\w')
2895+
t = p.compile_template('a')
2896+
self.assertIsInstance(t, re.Template)
2897+
self.assertEqual(re.sub(p, t, 'x-yz'), 'a-aa')
2898+
self.assertEqual(p.sub(t, 'x-yz'), 'a-aa')
2899+
self.assertEqual(re.subn(p, t, 'x-yz', count=2), ('a-az', 2))
2900+
self.assertEqual(p.subn(t, 'x-yz', 2), ('a-az', 2))
2901+
2902+
p = re.compile(br'\w')
2903+
t = p.compile_template(b'a')
2904+
self.assertIsInstance(t, re.Template)
2905+
self.assertEqual(re.sub(p, t, b'x-yz'), b'a-aa')
2906+
self.assertEqual(p.sub(t, b'x-yz'), b'a-aa')
2907+
self.assertEqual(re.subn(p, t, b'x-yz', count=2), (b'a-az', 2))
2908+
self.assertEqual(p.subn(t, b'x-yz', 2), (b'a-az', 2))
2909+
2910+
def test_group_refs(self):
2911+
p = re.compile(r'(\w)(\w)')
2912+
t = p.compile_template(r'[\2-\1]')
2913+
self.assertIsInstance(t, re.Template)
2914+
self.assertEqual(re.sub(p, t, 'xyzt'), '[y-x][t-z]')
2915+
self.assertEqual(p.sub(t, 'xyzt'), '[y-x][t-z]')
2916+
2917+
p = re.compile(br'(\w)(\w)')
2918+
t = p.compile_template(br'[\2-\1]')
2919+
self.assertIsInstance(t, re.Template)
2920+
self.assertEqual(re.sub(p, t, b'xyzt'), b'[y-x][t-z]')
2921+
self.assertEqual(p.sub(t, b'xyzt'), b'[y-x][t-z]')
2922+
2923+
def test_group_refs_emplty_literals(self):
2924+
p = re.compile(r'(\w)(\w)')
2925+
t = p.compile_template(r'\2\1')
2926+
self.assertIsInstance(t, re.Template)
2927+
self.assertEqual(re.sub(p, t, 'xyzt'), 'yxtz')
2928+
self.assertEqual(p.sub(t, 'xyzt'), 'yxtz')
2929+
2930+
p = re.compile(br'(\w)(\w)')
2931+
t = p.compile_template(br'\2\1')
2932+
self.assertIsInstance(t, re.Template)
2933+
self.assertEqual(re.sub(p, t, b'xyzt'), b'yxtz')
2934+
self.assertEqual(p.sub(t, b'xyzt'), b'yxtz')
2935+
2936+
def test_symbolic_group_refs(self):
2937+
p = re.compile(r'(?P<a>\w)(?P<b>\w)')
2938+
t = p.compile_template(r'[\g<b>-\g<a>]')
2939+
self.assertIsInstance(t, re.Template)
2940+
self.assertEqual(re.sub(p, t, 'xyzt'), '[y-x][t-z]')
2941+
self.assertEqual(p.sub(t, 'xyzt'), '[y-x][t-z]')
2942+
2943+
p = re.compile(br'(?P<a>\w)(?P<b>\w)')
2944+
t = p.compile_template(br'[\g<b>-\g<a>]')
2945+
self.assertIsInstance(t, re.Template)
2946+
self.assertEqual(re.sub(p, t, b'xyzt'), b'[y-x][t-z]')
2947+
self.assertEqual(p.sub(t, b'xyzt'), b'[y-x][t-z]')
2948+
2949+
def test_call(self):
2950+
p = re.compile(r'(\w)(\w)')
2951+
t = p.compile_template(r'[\2-\1]')
2952+
m = p.search(' xy ')
2953+
self.assertEqual(t(m), '[y-x]')
2954+
self.assertRaises(TypeError, t, None)
2955+
self.assertRaises(TypeError, t, {})
2956+
2957+
p = re.compile(br'(\w)(\w)')
2958+
t = p.compile_template(br'[\2-\1]')
2959+
m = p.search(b' xy ')
2960+
self.assertEqual(t(m), b'[y-x]')
2961+
self.assertRaises(TypeError, t, None)
2962+
self.assertRaises(TypeError, t, {})
2963+
2964+
28922965
class ImplementationTest(unittest.TestCase):
28932966
"""
28942967
Test implementation details of the re module.
@@ -2901,6 +2974,8 @@ def test_immutable(self):
29012974
re.Match.foo = 1
29022975
with self.assertRaises(TypeError):
29032976
re.Pattern.foo = 1
2977+
with self.assertRaises(TypeError):
2978+
re.Template.foo = 1
29042979
with self.assertRaises(TypeError):
29052980
pat = re.compile("")
29062981
tp = type(pat.scanner(""))
@@ -2924,6 +2999,7 @@ def test_disallow_instantiation(self):
29242999
# Ensure that the type disallows instantiation (bpo-43916)
29253000
check_disallow_instantiatio 1241 n(self, re.Match)
29263001
check_disallow_instantiation(self, re.Pattern)
3002+
check_disallow_instantiation(self, re.Template)
29273003
pat = re.compile("")
29283004
check_disallow_instantiation(self, type(pat.scanner("")))
29293005

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add the :meth:`~re.Pattern.compile_template` method for the :ref:`pattern
2+
object <re-objects>` which allows to pre-compile replacement strings.

Modules/_sre/clinic/sre.c.h

Lines changed: 46 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
0