r"""Converters from :class:`str` to another type.
These functions can be used for the ``type`` parameter in methods of
:class:`~argparse.ArgumentParser` and :class:`~argparsebuilder.ArgParseBuilder`
or similar use cases (if necessary in combination with
:func:`functools.partial`).
"""
import locale
import re
from datetime import datetime, date, time
from importlib.resources import read_text
from operator import le, ge
__version__ = read_text(__package__, 'VERSION').strip()
__all__ = ['booleans', 'use_locale_default', 'bool_conv', 'int_conv',
'float_conv', 'factor_conv', 'duration', 'datetime_conv',
'date_conv', 'time_conv', 'range_conv', 'sequence']
use_locale_default = False
"""Default value for :func:`int_conv` and :func:`float_conv`
for parameter ``use_locale``."""
booleans = {
'false': False,
'f': False,
'no': False,
'n': False,
'0': False,
'off': False,
'true': True,
't': True,
'yes': True,
'y': True,
'1': True,
'on': True,
} #: Mapping from strings to boolean values.
[docs]def bool_conv(string, *, values=None):
"""Convert a string to a boolean value.
The ``string`` is converted to lower case before
looking up the boolean value in ``values`` or :data:`booleans`
(if ``values`` is ``None``).
:param str string: input string
:value dict: mapping from strings to booleans
:return: converted value
:rtype: bool
:raises ValueError: if the string cannot be converted
.. versionchanged:: 0.2.0 Renamed (old name: boolean)
"""
if values is None:
values = booleans
try:
return values[string.lower()]
except KeyError:
raise ValueError(f'invalid boolean: {string!r}') from None
[docs]def int_conv(string, *, base=10, pred=None, use_locale=None):
"""Convert a string to an integer value.
>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
'de_DE.UTF-8'
>>> int_conv('1.234', use_locale=True)
1234
>>> locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
'en_US.UTF-8'
>>> int_conv('1,234', use_locale=True)
1234
>>> int_conv('-1', pred=lambda x: x > 0)
Traceback (most recent call last):
...
ValueError: invalid value: '-1'
See :class:`int` for an explanation of parameter ``base``.
:param str string: input string
:param int base: base (>= 2 and <= 36, or 0)
:param pred: predicate function
:param use_locale: ``True`` use current locale; ``None`` use
:data:`use_locale_default`
:type use_locale: bool or None
:return: converted value
:rtype: int
:raises ValueError: if the string cannot be converted
"""
if (use_locale if use_locale is not None else use_locale_default):
string = locale.delocalize(string)
x = int(string, base=base)
if not pred or pred(x):
return x
raise ValueError(f'invalid value: {string!r}')
[docs]def float_conv(string, *, base=10, pred=None, use_locale=None):
"""Convert a string to a float value.
:param str string: input string
:param int base: base (>= 2 and <= 36, or 0)
:param pred: predicate function
:param use_locale: ``True`` use current locale; ``None`` use
:data:`use_locale_default`
:type use_locale: bool or None
:return: converted value
:rtype: float
:raises ValueError: if the string cannot be converted
"""
if (use_locale if use_locale is not None else use_locale_default):
string = locale.delocalize(string)
if base != 10:
if not (base == 0 or 2 <= base <= 36):
raise ValueError('base must be >= 2 and <= 36, or 0')
a, *b = string.rsplit('.', 1)
try:
if b:
if base == 0:
prefix = a[:2].lower()
if prefix == '0b':
devisor = 2**(len(b[0]))
elif prefix == '0o':
devisor = 8**(len(b[0]))
elif prefix == '0x':
devisor = 16**(len(b[0]))
else:
devisor = 1
else:
prefix = ''
devisor = base**(len(b[0]))
frac = int(prefix + b[0], base=base) / devisor
else:
frac = 0.0
x = int(a, base=base) + frac
except ValueError:
raise ValueError(
f'could not convert string to float: {string!r}') from None
else:
x = float(string)
if not pred or pred(x):
return x
raise ValueError(f'invalid value: {string!r}')
[docs]def factor_conv(string, *, conv, factors):
"""Convert a string with a factor.
The symbols from ``factors`` are compared to the end of ``string``
until one matches. The ``string`` is then shortend by the the length
of the symbol and the rest converted with ``conv`` and multiplied by
the factor that corresponds to the symbol.
>>> factors = {'h': 3600, 'm': 60, 's': 1}
>>> factor_conv('10m', conv=int, factors=factors)
600
:param str string: input string
:param conv: converter function
:param dict factors: mapping from symbol to factor
:return: converted value
:raises ValueError: if the string cannot be converted
"""
for sym in factors:
if string.endswith(sym):
if sym:
return conv(string[:-len(sym)]) * factors[sym]
else:
return conv(string) * factors[sym]
raise ValueError(f'invalid value: {string!r}')
[docs]def datetime_conv(string, *, format=None):
"""Convert a string to a :class:`datetime.datetime`.
If ``format=None`` this function uses
:meth:`datetime.datetime.fromisoformat` else
:meth:`datetime.datetime.strptime`.
:param str string: datetime string
:param format: format string
:type format: str or None
:return: converted datetime
:rtype: datetime.datetime
:raises ValueError: if the string cannot be converted
.. versionchanged:: 0.2.0 Renamed (old name: datetime)
"""
if format is None:
return datetime.fromisoformat(string)
else:
return datetime.strptime(string, format)
[docs]def date_conv(string, *, format=None):
"""Convert a string to a class:`datetime.date`.
If ``format=None`` this function uses
:meth:`datetime.date.fromisoformat` else
:meth:`datetime.datetime.strptime`.
:param str string: date string
:param format: format string
:return: converted date
:rtype: datetime.date
:raises ValueError: if the string cannot be converted
.. versionchanged:: 0.2.0 Renamed (old name: date)
"""
if format is None:
return date.fromisoformat(string)
else:
return datetime.strptime(string, format).date()
[docs]def time_conv(string, *, format=None):
"""Convert a string to a :class:`datetime.time`.
If ``format=None`` this function uses
:meth:`datetime.time.fromisoformat` else
:meth:`datetime.datetime.strptime`.
:param str string: time string
:param format: format string
:return: converted time
:rtype: datetime.time
:raises ValueError: if the string cannot be converted
.. versionchanged:: 0.2.0 Renamed (old name: time)
"""
if format is None:
return time.fromisoformat(string)
else:
return datetime.strptime(string, format).time()
[docs]def duration(string, *, use_locale=None):
"""Convert duration string to seconds.
Format: [[H:]M:]S[.f]
See also: :func:`salmagundi.strings.parse_timedelta`
:param str string: duration string
:param use_locale: ``True`` use current locale; ``None`` use
:data:`use_locale_default`
:type use_locale: bool or None
:return: converted duration
:rtype: float
:raises ValueError: if the string cannot be converted
.. versionchanged:: 0.1.1 Add parameter ``use_locale``
"""
h, m, s = 0, 0, 0
a = string.split(':')
try:
s = float_conv(a[-1], pred=lambda x: 0.0 <= x < 60.0,
use_locale=use_locale)
if len(a) >= 2:
m = int_conv(a[-2], pred=lambda x: 0 <= x < 60)
if len(a) == 3:
h = int_conv(a[0], pred=lambda x: 0 <= x, use_locale=use_locale)
if len(a) > 3:
raise ValueError
except ValueError:
raise ValueError(f'invalid duration: {string!r}')
return h * 3600.0 + m * 60.0 + s
_num_re = r'(?:\d(?:\.\d*)*)+'
_range_re = (fr'^\s*(?P<start>[-+]?{_num_re})\s*%s\s*'
fr'(?P<end>[-+]?{_num_re})'
fr'(?:\s*/\s*(?P<step>[-+]?{_num_re}))?\s*$')
[docs]def range_conv(string, *, conv=None, separator='-'):
"""Convert a range string.
Range string: '<start><separator><end>[/<step>]' (default: <step> = 1)
>>> list(range_conv('-2-4'))
[-2, -1, 0, 1, 2, 3, 4]
>>> list(range_conv('-2-4/2'))
[-2, 0, 2, 4]
>>> list(range_conv('4--2/-2'))
[4, 2, 0, -2]
>>> list(range_conv('4..-2/-2', separator='..'))
[4, 2, 0, -2]
>>> from itertools import chain
>>> list(chain.from_iterable(sequence('1, -2-4/2, 42', conv=range_conv)))
[1, -2, 0, 2, 4, 42]
:param str string: input string
:param conv: converter function which returns an int or a float value
(default: ``int``)
:param str separator: separator between <start> and <end>
:return: generator that yields converted values
:raises ValueError: if the string cannot be converted
.. versionadded:: 0.2.0
.. versionchanged:: 0.2.1 Add parameter ``separator``
"""
if not conv:
conv = int
if m := re.match(_range_re % re.escape(separator), string):
start = conv(m['start'])
if not isinstance(start, (int, float)):
raise ValueError('conv must return int or float')
if m['step']:
step = conv(m['step'])
if not step:
raise ValueError('step cannot be zero')
else:
step = 1 if isinstance(start, int) else 1.0
end = conv(m['end'])
cmp = le if step > 0 else ge
x = start
while cmp(x, end):
yield x
x += step
else:
yield conv(string)
[docs]def sequence(string, *, conv=None, separator=','):
"""Convert a sequence string.
>>> sequence('1, 2, 3', conv=float)
(1.0, 2.0, 3.0)
:param str string: input string
:param conv: converter function
:param str separator: separator between elements of sequence
:return: generator that yields converted values
:raises ValueError: if the string cannot be converted
.. versionadded:: 0.2.0
"""
for s in string.split(separator):
yield conv(s)