gh-128641: Fix ConfigParser.read Perfomance Regression (#129596)

---------

Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
This commit is contained in:
Andrew Shteren 2025-02-24 03:20:37 +03:00 committed by GitHub
parent 071820113f
commit cd6abe27a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 25 additions and 29 deletions

View file

@ -154,7 +154,6 @@ import itertools
import os import os
import re import re
import sys import sys
import types
__all__ = ("NoSectionError", "DuplicateOptionError", "DuplicateSectionError", __all__ = ("NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
"NoOptionError", "InterpolationError", "InterpolationDepthError", "NoOptionError", "InterpolationError", "InterpolationDepthError",
@ -570,35 +569,36 @@ class _ReadState:
class _Line(str): class _Line(str):
__slots__ = 'clean', 'has_comments'
def __new__(cls, val, *args, **kwargs): def __new__(cls, val, *args, **kwargs):
return super().__new__(cls, val) return super().__new__(cls, val)
def __init__(self, val, prefixes): def __init__(self, val, comments):
self.prefixes = prefixes trimmed = val.strip()
self.clean = comments.strip(trimmed)
self.has_comments = trimmed != self.clean
@functools.cached_property
def clean(self):
return self._strip_full() and self._strip_inline()
@property class _CommentSpec:
def has_comments(self): def __init__(self, full_prefixes, inline_prefixes):
return self.strip() != self.clean full_patterns = (
# prefix at the beginning of a line
def _strip_inline(self): fr'^({re.escape(prefix)}).*'
""" for prefix in full_prefixes
Search for the earliest prefix at the beginning of the line or following a space.
"""
matcher = re.compile(
'|'.join(fr'(^|\s)({re.escape(prefix)})' for prefix in self.prefixes.inline)
# match nothing if no prefixes
or '(?!)'
) )
match = matcher.search(self) inline_patterns = (
return self[:match.start() if match else None].strip() # prefix at the beginning of the line or following a space
fr'(^|\s)({re.escape(prefix)}.*)'
for prefix in inline_prefixes
)
self.pattern = re.compile('|'.join(itertools.chain(full_patterns, inline_patterns)))
def _strip_full(self): def strip(self, text):
return '' if any(map(self.strip().startswith, self.prefixes.full)) else True return self.pattern.sub('', text).rstrip()
def wrap(self, text):
return _Line(text, self)
class RawConfigParser(MutableMapping): class RawConfigParser(MutableMapping):
@ -667,10 +667,7 @@ class RawConfigParser(MutableMapping):
else: else:
self._optcre = re.compile(self._OPT_TMPL.format(delim=d), self._optcre = re.compile(self._OPT_TMPL.format(delim=d),
re.VERBOSE) re.VERBOSE)
self._prefixes = types.SimpleNamespace( self._comments = _CommentSpec(comment_prefixes or (), inline_comment_prefixes or ())
full=tuple(comment_prefixes or ()),
inline=tuple(inline_comment_prefixes or ()),
)
self._strict = strict self._strict = strict
self._allow_no_value = allow_no_value self._allow_no_value = allow_no_value
self._empty_lines_in_values = empty_lines_in_values self._empty_lines_in_values = empty_lines_in_values
@ -1066,7 +1063,6 @@ class RawConfigParser(MutableMapping):
in an otherwise empty line or may be entered in lines holding values or in an otherwise empty line or may be entered in lines holding values or
section names. Please note that comments get stripped off when reading configuration files. section names. Please note that comments get stripped off when reading configuration files.
""" """
try: try:
ParsingError._raise_all(self._read_inner(fp, fpname)) ParsingError._raise_all(self._read_inner(fp, fpname))
finally: finally:
@ -1075,8 +1071,7 @@ class RawConfigParser(MutableMapping):
def _read_inner(self, fp, fpname): def _read_inner(self, fp, fpname):
st = _ReadState() st = _ReadState()
Line = functools.partial(_Line, prefixes=self._prefixes) for st.lineno, line in enumerate(map(self._comments.wrap, fp), start=1):
for st.lineno, line in enumerate(map(Line, fp), start=1):
if not line.clean: if not line.clean:
if self._empty_lines_in_values: if self._empty_lines_in_values:
# add empty line to the value, but only if there was no # add empty line to the value, but only if there was no

View file

@ -0,0 +1 @@
Restore :meth:`configparser.ConfigParser.read` performance.