Source code for kwutil.partial_format

"""
Format strings in a bash-like way with :func:`subtemplate`.

Partially format python format strings with variables that exist using
:func:`partial_format`.
"""
import string


[docs] def partial_format(format_string, *args, **kwargs): """ A solution to the partial string formatting problem. Taken from [SO11283961]_, which is a modification of the stdlib string.Formatter code. Args: format_string (str): the templated string to be formatted *args: positional replacements **kwargs: key value replacements Returns: str : the format string with only the specified parts replaced. Example: >>> from kwutil.partial_format import partial_format >>> format_string = '{foo} + {bar} = {baz}' >>> args = tuple() >>> kwargs = dict(bar=3) >>> partial_format(format_string, *args, **kwargs) '{foo} + 3 = {baz}' References: [SO11283961] https://stackoverflow.com/questions/11283961/partial-string-formatting """ return _PartialFormatter().format(format_string, *args, **kwargs)
[docs] class _PartialFormatter(string.Formatter): """ A modified string formatter that handles a partial set of format args/kwargs. """
[docs] def vformat(self, format_string, args, kwargs): used_args = set() result, _ = self._vformat(format_string, args, kwargs, used_args, 2) self.check_unused_args(used_args, args, kwargs) return result
[docs] def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, auto_arg_index=0): if recursion_depth < 0: raise ValueError('Max string recursion exceeded') result = [] for literal_text, field_name, format_spec, conversion in \ self.parse(format_string): orig_field_name = field_name # output the literal text if literal_text: result.append(literal_text) # if there's a field, output it if field_name is not None: # this is some markup, find the object and do # the formatting # handle arg indexing when empty field_names are given. if field_name == '': if auto_arg_index is False: raise ValueError('cannot switch from manual field ' 'specification to automatic field ' 'numbering') field_name = str(auto_arg_index) auto_arg_index += 1 elif field_name.isdigit(): if auto_arg_index: raise ValueError('cannot switch from manual field ' 'specification to automatic field ' 'numbering') # disable auto arg incrementing, if it gets # used later on, then an exception will be raised auto_arg_index = False # given the field_name, find the object it references # and the argument it came from try: obj, arg_used = self.get_field(field_name, args, kwargs) except (IndexError, KeyError): ########################## # Where the magic happens. # ------------------------ # This case is the main difference between this class and # the stdlib implementation. ########################## # catch issues with both arg indexing and kwarg key errors obj = orig_field_name if conversion: obj += '!{}'.format(conversion) if format_spec: format_spec, auto_arg_index = self._vformat( format_spec, args, kwargs, used_args, recursion_depth, auto_arg_index=auto_arg_index) obj += ':{}'.format(format_spec) result.append('{' + obj + '}') else: used_args.add(arg_used) # do any conversion on the resulting object obj = self.convert_field(obj, conversion) # expand the format spec, if needed format_spec, auto_arg_index = self._vformat( format_spec, args, kwargs, used_args, recursion_depth - 1, auto_arg_index=auto_arg_index) # format the object and append to the result result.append(self.format_field(obj, format_spec)) return ''.join(result), auto_arg_index
[docs] def _test_partial_format(): """ Example: _test_partial_format """ import pytest def test_auto_indexing(): # test basic arg auto-indexing assert partial_format('{}{}', 4, 2) == '42' assert partial_format('{}{} {}', 4, 2) == '42 {}' def test_manual_indexing(): # test basic arg indexing assert partial_format('{0}{1} is not {1} or {0}', 4, 2) == '42 is not 2 or 4' assert partial_format('{0}{1} is {3} {1} or {0}', 4, 2) == '42 is {3} 2 or 4' def test_mixing_manualauto_fails(): # test mixing manual and auto args raises with pytest.raises(ValueError): assert partial_format('{!r} is {0}{1}', 4, 2) def test_kwargs(): # test basic kwarg assert partial_format('{base}{n}', base=4, n=2) == '42' assert partial_format('{base}{n}', base=4, n=2, extra='foo') == '42' assert partial_format('{base}{n} {key}', base=4, n=2) == '42 {key}' def test_args_and_kwargs(): # test mixing args/kwargs with leftovers assert partial_format('{}{k} {v}', 4, k=2) == '42 {v}' # test mixing with leftovers r = partial_format('{}{} is the {k} to {!r}', 4, 2, k='answer') assert r == '42 is the answer to {!r}' def test_coercion(): # test coercion is preserved for skipped elements assert partial_format('{!r} {k!r}', '42') == "'42' {k!r}" def test_nesting(): # test nesting works with or with out parent keys assert partial_format('{k:>{size}}', k=42, size=3) == ' 42' assert partial_format('{k:>{size}}', size=3) == '{k:>3}' test_mixing_manualauto_fails() test_auto_indexing() test_manual_indexing() test_kwargs() test_args_and_kwargs() test_coercion() test_nesting() cases = [ ('{a} {b}', '1 2.0'), ('{z} {y}', '{z} {y}'), ('{a} {a:2d} {a:04d} {y:2d} {z:04d}', '1 1 0001 {y:2d} {z:04d}'), ('{a!s} {z!s} {d!r}', '1 {z!s} {\'k\': \'v\'}'), ('{a!s:>2s} {z!s:>2s}', ' 1 {z!s:>2s}'), ('{a!s:>{a}s} {z!s:>{z}s}', '1 {z!s:>{z}s}'), ('{a.imag} {z.y}', '0 {z.y}'), ('{e[0]:03d} {z[0]:03d}', '042 {z[0]:03d}'), ] for s, expected in cases: # test a bunch of random stuff data = dict( a=1, b=2.0, c='3', d={'k': 'v'}, e=[42], ) result = partial_format(s, **data) assert expected == result
# def subtemplate(text, subs=None, /, **kwargs):
[docs] def subtemplate(*args, **kwargs): """ Substitutes variables into a templated string. Similar to format, replaces variables with bash-like dollar sign patterns (e.g. $VARNAME or ${VARNAME}). Args: text (str): the templated text subs (dict | None): a dictionary of substitutions **kwargs: other substitutions Returns: str: formatted text. Unspecified variables are left unchanged. Notes: Order of precedence from lowest to highest goes: subs, kwargs Example: >>> from kwutil.partial_format import * # NOQA >>> import ubelt as ub >>> text = ub.codeblock( >>> ''' >>> The $SUBJECT $VERB the $OBJECT >>> ''') >>> print(subtemplate(text, SUBJECT='dog', OBJECT='food')) The dog $VERB the food >>> print(subtemplate(text, SUBJECT='dog', VERB='eats', OBJECT='food')) The dog eats the food >>> print(subtemplate(text, {'SUBJECT': 'dude'}, SUBJECT='dog')) The dog $VERB the $OBJECT """ if len(args) == 1: text = args[0] subs = None elif len(args) == 2: text, subs = args else: raise ValueError('Must have either 1 or 2 arguments (text, subs)') import string import ubelt as ub import operator as op from functools import reduce fmtdict = ub.udict({}) if subs: fmtdict.update(subs) fmtdict.update(kwargs) if not fmtdict: return text template = string.Template(text) existing_vars = {reduce(op.add, t, '') for t in template.pattern.findall(text)} fmtdict = fmtdict & existing_vars return template.safe_substitute(**fmtdict)
# def fsubtemplate(text, subs=None, /, **kwargs):
[docs] def fsubtemplate(*args, **kwargs): """ Like subtemplate, but uses the current local variable context Args: text (str): the templated text subs (dict | None): a dictionary of substitutions **kwargs: other substitutions Returns: str: formatted text. Unspecified variables are left unchanged. Notes: Order of precedence from lowest to highest goes: locals, subs, kwargs Example: >>> from kwutil.partial_format import * # NOQA >>> import ubelt as ub >>> text = ub.codeblock( >>> ''' >>> The $SUBJECT $VERB the $OBJECT >>> ''') >>> SUBJECT = 'dude' >>> print(fsubtemplate(text, OBJECT='food')) The dude $VERB the food >>> print(fsubtemplate(text, SUBJECT='dog', OBJECT='food')) The dog $VERB the food >>> print(fsubtemplate(text, SUBJECT='dog', VERB='eats', OBJECT='food')) The dog eats the food """ if len(args) == 1: text = args[0] subs = None elif len(args) == 2: text, subs = args else: raise ValueError('Must have either 1 or 2 arguments (text, subs)') import ubelt as ub locals_ = _get_stack_frame(1).f_locals subs = ub.udict() subs.update(locals_) if subs is not None: subs.update(subs) subs.update(kwargs) return subtemplate(text, subs)
[docs] def _get_stack_frame(N=0, strict=True): """ Args: N (int): N=0 means the frame you called this function in. N=1 is the parent frame. strict (bool): (default = True) """ import inspect frame_cur = inspect.currentframe() for idx in range(N + 1): # always skip the frame of this function frame_next = frame_cur.f_back if frame_next is None: if strict: raise AssertionError('Frame level {:!r} is root'.format(idx)) else: break frame_cur = frame_next return frame_cur