Skip to content

Commit dfdd2f7

Browse files
committed
Closes #18159: ConfigParser getters not available on SectionProxy
1 parent 34cea14 commit dfdd2f7

3 files changed

Lines changed: 386 additions & 53 deletions

File tree

Doc/library/configparser.rst

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,8 @@ Boolean values from ``'yes'``/``'no'``, ``'on'``/``'off'``,
162162
True
163163

164164
Apart from :meth:`getboolean`, config parsers also provide equivalent
165-
:meth:`getint` and :meth:`getfloat` methods, but these are far less
166-
useful since conversion using :func:`int` and :func:`float` is
167-
sufficient for these types.
168-
165+
:meth:`getint` and :meth:`getfloat` methods. You can register your own
166+
converters and customize the provided ones. [1]_
169167

170168
Fallback Values
171169
---------------
@@ -555,10 +553,10 @@ the :meth:`__init__` options:
555553

556554
Comment prefixes are strings that indicate the start of a valid comment within
557555
a config file. *comment_prefixes* are used only on otherwise empty lines
558-
(optionally indented) whereas *inline_comment_prefixes* can be used
559-
after every valid value (e.g. section names, options and empty lines
560-
as well). By default inline comments are disabled and ``'#'`` and
561-
``';'`` are used as prefixes for whole line comments.
556+
(optionally indented) whereas *inline_comment_prefixes* can be used after
557+
every valid value (e.g. section names, options and empty lines as well). By
558+
default inline comments are disabled and ``'#'`` and ``';'`` are used as
559+
prefixes for whole line comments.
562560

563561
.. versionchanged:: 3.2
564562
In previous versions of :mod:`configparser` behaviour matched
@@ -667,10 +665,26 @@ the :meth:`__init__` options:
667665
`dedicated documentation section <#interpolation-of-values>`_.
668666
:class:`RawConfigParser` has a default value of ``None``.
669667

668+
* *converters*, default value: not set
669+
670+
Config parsers provide option value getters that perform type conversion. By
671+
default :meth:`getint`, :meth:`getfloat`, and :meth:`getboolean` are
672+
implemented. Should other getters be desirable, users may define them in
673+
a subclass or pass a dictionary where each key is a name of the converter and
674+
each value is a callable implementing said conversion. For instance, passing
675+
``{'decimal': decimal.Decimal}`` would add :meth:`getdecimal` on both the
676+
parser object and all section proxies. In other words, it will be possible
677+
to write both ``parser_instance.getdecimal('section', 'key', fallback=0)``
678+
and ``parser_instance['section'].getdecimal('key', 0)``.
679+
680+
If the converter needs to access the state of the parser, it can be
681+
implemented as a method on a config parser subclass. If the name of this
682+
method starts with ``get``, it will be available on all section proxies, in
683+
the dict-compatible form (see the ``getdecimal()`` example above).
670684

671685
More advanced customization may be achieved by overriding default values of
672-
these parser attributes. The defaults are defined on the classes, so they
673-
may be overridden by subclasses or by attribute assignment.
686+
these parser attributes. The defaults are defined on the classes, so they may
687+
be overridden by subclasses or by attribute assignment.
674688

675689
.. attribute:: BOOLEAN_STATES
676690

@@ -863,7 +877,7 @@ interpolation if an option used is not defined elsewhere. ::
863877
ConfigParser Objects
864878
--------------------
865879

866-
.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation())
880+
.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation(), converters={})
867881

868882
The main configuration parser. When *defaults* is given, it is initialized
869883
into the dictionary of intrinsic defaults. When *dict_type* is given, it
@@ -903,6 +917,12 @@ ConfigParser Objects
903917
converts option names to lower case), the values ``foo %(bar)s`` and ``foo
904918
%(BAR)s`` are equivalent.
905919

920+
When *converters* is given, it should be a dictionary where each key
921+
represents the name of a type converter and each value is a callable
922+
implementing the conversion from string to the desired datatype. Every
923+
converter gets its own corresponding :meth:`get*()` method on the parser
924+
object and section proxies.
925+
906926
.. versionchanged:: 3.1
907927
The default *dict_type* is :class:`collections.OrderedDict`.
908928

@@ -911,6 +931,9 @@ ConfigParser Objects
911931
*empty_lines_in_values*, *default_section* and *interpolation* were
912932
added.
913933

934+
.. versionchanged:: 3.5
935+
The *converters* argument was added.
936+
914937

915938
.. method:: defaults()
916939

@@ -1286,3 +1309,4 @@ Exceptions
12861309
.. [1] Config parsers allow for heavy customization. If you are interested in
12871310
changing the behaviour outlined by the footnote reference, consult the
12881311
`Customizing Parser Behaviour`_ section.
1312+

Lib/configparser.py

Lines changed: 128 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
__init__(defaults=None, dict_type=_default_dict, allow_no_value=False,
1818
delimiters=('=', ':'), comment_prefixes=('#', ';'),
1919
inline_comment_prefixes=None, strict=True,
20-
empty_lines_in_values=True):
20+
empty_lines_in_values=True, default_section='DEFAULT',
21+
interpolation=<unset>, converters=<unset>):
2122
Create the parser. When `defaults' is given, it is initialized into the
2223
dictionary or intrinsic defaults. The keys must be strings, the values
2324
must be appropriate for %()s string interpolation.
@@ -47,6 +48,25 @@
4748
When `allow_no_value' is True (default: False), options without
4849
values are accepted; the value presented for these is None.
4950
51+
When `default_section' is given, the name of the special section is
52+
named accordingly. By default it is called ``"DEFAULT"`` but this can
53+
be customized to point to any other valid section name. Its current
54+
value can be retrieved using the ``parser_instance.default_section``
55+
attribute and may be modified at runtime.
56+
57+
When `interpolation` is given, it should be an Interpolation subclass
58+
instance. It will be used as the handler for option value
59+
pre-processing when using getters. RawConfigParser object s don't do
60+
any sort of interpolation, whereas ConfigParser uses an instance of
61+
BasicInterpolation. The library also provides a ``zc.buildbot``
62+
inspired ExtendedInterpolation implementation.
63+
64+
When `converters` is given, it should be a dictionary where each key
65+
represents the name of a type converter and each value is a callable
66+
implementing the conversion from string to the desired datatype. Every
67+
converter gets its corresponding get*() method on the parser object and
68+
section proxies.
69+
5070
sections()
5171
Return all the configuration section names, sans DEFAULT.
5272
@@ -129,9 +149,11 @@
129149

130150
__all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
131151
"NoOptionError", "InterpolationError", "InterpolationDepthError",
132-
"InterpolationSyntaxError", "ParsingError",
133-
"MissingSectionHeaderError",
152+
"InterpolationMissingOptionError", "InterpolationSyntaxError",
153+
"ParsingError", "MissingSectionHeaderError",
134154
"ConfigParser", "SafeConfigParser", "RawConfigParser",
155+
"Interpolation", "BasicInterpolation", "ExtendedInterpolation",
156+
"LegacyInterpolation", "SectionProxy", "ConverterMapping",
135157
"DEFAULTSECT", "MAX_INTERPOLATION_DEPTH"]
136158

137159
DEFAULTSECT = "DEFAULT"
@@ -580,11 +602,12 @@ def __init__(self, defaults=None, dict_type=_default_dict,
580602
comment_prefixes=('#', ';'), inline_comment_prefixes=None,
581603
strict=True, empty_lines_in_values=True,
582604
default_section=DEFAULTSECT,
583-
interpolation=_UNSET):
605+
interpolation=_UNSET, converters=_UNSET):
584606

585607
self._dict = dict_type
586608
self._sections = self._dict()
587609
self._defaults = self._dict()
610+
self._converters = ConverterMapping(self)
588611
self._proxies = self._dict()
589612
self._proxies[default_section] = SectionProxy(self, default_section)
590613
if defaults:
@@ -612,6 +635,8 @@ def __init__(self, defaults=None, dict_type=_default_dict,
612635
self._interpolation = self._DEFAULT_INTERPOLATION
613636
if self._interpolation is None:
614637
self._interpolation = Interpolation()
638+
if converters is not _UNSET:
639+
self._converters.update(converters)
615640

616641
def defaults(self):
617642
return self._defaults
@@ -775,36 +800,31 @@ def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
775800
def _get(self, section, conv, option, **kwargs):
776801
return conv(self.get(section, option, **kwargs))
777802

778-
def getint(self, section, option, *, raw=False, vars=None,
779-
fallback=_UNSET):
803+
def _get_conv(self, section, option, conv, *, raw=False, vars=None,
804+
fallback=_UNSET, **kwargs):
780805
try:
781-
return self._get(section, int, option, raw=raw, vars=vars)
806+
return self._get(section, conv, option, raw=raw, vars=vars,
807+
**kwargs)
782808
except (NoSectionError, NoOptionError):
783809
if fallback is _UNSET:
784810
raise
785-
else:
786-
return fallback
811+
return fallback
812+
813+
# getint, getfloat and getboolean provided directly for backwards compat
814+
def getint(self, section, option, *, raw=False, vars=None,
815+
fallback=_UNSET, **kwargs):
816+
return self._get_conv(section, option, int, raw=raw, vars=vars,
817+
fallback=fallback, **kwargs)
787818

788819
def getfloat(self, section, option, *, raw=False, vars=None,
789-
fallback=_UNSET):
790-
try:
791-
return self._get(section, float, option, raw=raw, vars=vars)
792-
except (NoSectionError, NoOptionError):
793-
if fallback is _UNSET:
794-
raise
795-
else:
796-
return fallback
820+
fallback=_UNSET, **kwargs):
821+
return self._get_conv(section, option, float, raw=raw, vars=vars,
822+
fallback=fallback, **kwargs)
797823

798824
def getboolean(self, section, option, *, raw=False, vars=None,
799-
fallback=_UNSET):
800-
try:
801-
return self._get(section, self._convert_to_boolean, option,
802-
raw=raw, vars=vars)
803-
except (NoSectionError, NoOptionError):
804-
if fallback is _UNSET:
805-
raise
806-
else:
807-
return fallback
825+
fallback=_UNSET, **kwargs):
826+
return self._get_conv(section, option, self._convert_to_boolean,
827+
raw=raw, vars=vars, fallback=fallback, **kwargs)
808828

809829
def items(self, section=_UNSET, raw=False, vars=None):
810830
"""Return a list of (name, value) tuples for each option in a section.
@@ -1154,6 +1174,10 @@ def _validate_value_types(self, *, section="", option="", value=""):
11541174
if not isinstance(value, str):
11551175
raise TypeError("option values must be strings")
11561176

1177+
@property
1178+
def converters(self):
1179+
return self._converters
1180+
11571181

11581182
class ConfigParser(RawConfigParser):
11591183
"""ConfigParser implementing interpolation."""
@@ -1194,6 +1218,10 @@ def __init__(self, parser, name):
11941218
"""Creates a view on a section of the specified `name` in `parser`."""
11951219
self._parser = parser
11961220
self._name = name
1221+
for conv in parser.converters:
1222+
key = 'get' + conv
1223+
getter = functools.partial(self.get, _impl=getattr(parser, key))
1224+
setattr(self, key, getter)
11971225

11981226
def __repr__(self):
11991227
return '<Section: {}>'.format(self._name)
@@ -1227,22 +1255,6 @@ def _options(self):
12271255
else:
12281256
return self._parser.defaults()
12291257

1230-
def get(self, option, fallback=None, *, raw=False, vars=None):
1231-
return self._parser.get(self._name, option, raw=raw, vars=vars,
1232-
fallback=fallback)
1233-
1234-
def getint(self, option, fallback=None, *, raw=False, vars=None):
1235-
return self._parser.getint(self._name, option, raw=raw, vars=vars,
1236-
fallback=fallback)
1237-
1238-
def getfloat(self, option, fallback=None, *, raw=False, vars=None):
1239-
return self._parser.getfloat(self._name, option, raw=raw, vars=vars,
1240-
fallback=fallback)
1241-
1242-
def getboolean(self, option, fallback=None, *, raw=False, vars=None):
1243-
return self._parser.getboolean(self._name, option, raw=raw, vars=vars,
1244-
fallback=fallback)
1245-
12461258
@property
12471259
def parser(self):
12481260
# The parser object of the proxy is read-only.
@@ -1252,3 +1264,77 @@ def parser(self):
12521264
def name(self):
12531265
# The name of the section on a proxy is read-only.
12541266
return self._name
1267+
1268+
def get(self, option, fallback=None, *, raw=False, vars=None,
1269+
_impl=None, **kwargs):
1270+
"""Get an option value.
1271+
1272+
Unless `fallback` is provided, `None` will be returned if the option
1273+
is not found.
1274+
1275+
"""
1276+
# If `_impl` is provided, it should be a getter method on the parser
1277+
# object that provides the desired type conversion.
1278+
if not _impl:
1279+
_impl = self._parser.get
1280+
return _impl(self._name, option, raw=raw, vars=vars,
1281+
fallback=fallback, **kwargs)
1282+
1283+
1284+
class ConverterMapping(MutableMapping):
1285+
"""Enables reuse of get*() methods between the parser and section proxies.
1286+
1287+
If a parser class implements a getter directly, the value for the given
1288+
key will be ``None``. The presence of the converter name here enables
1289+
section proxies to find and use the implementation on the parser class.
1290+
"""
1291+
1292+
GETTERCRE = re.compile(r"^get(?P<name>.+)$")
1293+
1294+
def __init__(self, parser):
1295+
self._parser = parser
1296+
self._data = {}
1297+
for getter in dir(self._parser):
1298+
m = self.GETTERCRE.match(getter)
1299+
if not m or not callable(getattr(self._parser, getter)):
1300+
continue
1301+
self._data[m.group('name')] = None # See class docstring.
1302+
1303+
def __getitem__(self, key):
1304+
return self._data[key]
1305+
1306+
def __setitem__(self, key, value):
1307+
try:
1308+
k = 'get' + key
1309+
except TypeError:
1310+
raise ValueError('Incompatible key: {} (type: {})'
1311+
''.format(key, type(key)))
1312+
if k == 'get':
1313+
raise ValueError('Incompatible key: cannot use "" as a name')
1314+
self._data[key] = value
1315+
func = functools.partial(self._parser._get_conv, conv=value)
1316+
func.converter = value
1317+
setattr(self._parser, k, func)
1318+
for proxy in self._parser.values():
1319+
getter = functools.partial(proxy.get, _impl=func)
1320+
setattr(proxy, k, getter)
1321+
1322+
def __delitem__(self, key):
1323+
try:
1324+
k = 'get' + (key or None)
1325+
except TypeError:
1326+
raise KeyError(key)
1327+
del self._data[key]
1328+
for inst in itertools.chain((self._parser,), self._parser.values()):
1329+
try:
1330+
delattr(inst, k)
1331+
except AttributeError:
1332+
# don't raise since the entry was present in _data, silently
1333+
# clean up
1334+
continue
1335+
1336+
def __iter__(self):
1337+
return iter(self._data)
1338+
1339+
def __len__(self):
1340+
return len(self._data)

0 commit comments

Comments
 (0)