Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-118761: Optimise import time for string #132037

Merged
merged 6 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions Lib/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,18 @@ def capwords(s, sep=None):


####################################################################
import re as _re
from collections import ChainMap as _ChainMap

_sentinel_dict = {}


class _TemplatePattern:
# This descriptor is overwritten in ``Template._compile_pattern()``.
def __get__(self, instance, cls=None):
if cls is None:
return self
return cls._compile_pattern()
_TemplatePattern = _TemplatePattern()


class Template:
"""A string class for supporting $-substitutions."""

Expand All @@ -64,14 +71,21 @@ class Template:
# See https://bugs.python.org/issue31672
idpattern = r'(?a:[_a-z][_a-z0-9]*)'
braceidpattern = None
flags = _re.IGNORECASE
flags = None # default: re.IGNORECASE

pattern = _TemplatePattern # use a descriptor to compile the pattern

def __init_subclass__(cls):
super().__init_subclass__()
if 'pattern' in cls.__dict__:
pattern = cls.pattern
else:
delim = _re.escape(cls.delimiter)
cls._compile_pattern()

@classmethod
def _compile_pattern(cls):
import re # deferred import, for performance

pattern = cls.__dict__.get('pattern', _TemplatePattern)
if pattern is _TemplatePattern:
delim = re.escape(cls.delimiter)
id = cls.idpattern
bid = cls.braceidpattern or cls.idpattern
pattern = fr"""
Expand All @@ -82,7 +96,10 @@ def __init_subclass__(cls):
(?P<invalid>) # Other ill-formed delimiter exprs
)
"""
cls.pattern = _re.compile(pattern, cls.flags | _re.VERBOSE)
if cls.flags is None:
cls.flags = re.IGNORECASE
pat = cls.pattern = re.compile(pattern, cls.flags | re.VERBOSE)
return pat

def __init__(self, template):
self.template = template
Expand All @@ -105,7 +122,8 @@ def substitute(self, mapping=_sentinel_dict, /, **kws):
if mapping is _sentinel_dict:
mapping = kws
elif kws:
mapping = _ChainMap(kws, mapping)
from collections import ChainMap
mapping = ChainMap(kws, mapping)
# Helper function for .sub()
def convert(mo):
# Check the most common path first.
Expand All @@ -124,7 +142,8 @@ def safe_substitute(self, mapping=_sentinel_dict, /, **kws):
if mapping is _sentinel_dict:
mapping = kws
elif kws:
mapping = _ChainMap(kws, mapping)
from collections import ChainMap
mapping = ChainMap(kws, mapping)
# Helper function for .sub()
def convert(mo):
named = mo.group('named') or mo.group('braced')
Expand Down Expand Up @@ -170,10 +189,6 @@ def get_identifiers(self):
self.pattern)
return ids

# Initialize Template.pattern. __init_subclass__() is automatically called
# only for subclasses, not for the Template class itself.
Template.__init_subclass__()


########################################################################
# the Formatter class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve import times by up to 27x for the :mod:`string` module.
Patch by Adam Turner.
Loading