[diffoscope] 01/08: presenters: add a PartialString class
Ximin Luo
infinity0 at debian.org
Mon Jul 3 20:27:50 CEST 2017
This is an automated email from the git hooks/post-receive script.
infinity0 pushed a commit to branch WIP/humungous-diffs
in repository diffoscope.
commit fc9146843f02685c862db1e3ada2fd0ceaf90823
Author: Ximin Luo <infinity0 at debian.org>
Date: Fri Jun 16 22:56:42 2017 +0200
presenters: add a PartialString class
---
diffoscope/presenters/utils.py | 166 +++++++++++++++++++++++++++++++++++++++++
tests/test_presenters.py | 18 ++++-
2 files changed, 183 insertions(+), 1 deletion(-)
diff --git a/diffoscope/presenters/utils.py b/diffoscope/presenters/utils.py
index e69c5d4..b0da209 100644
--- a/diffoscope/presenters/utils.py
+++ b/diffoscope/presenters/utils.py
@@ -19,7 +19,10 @@
import sys
import codecs
+import collections
import contextlib
+import string
+import _string
class Presenter(object):
@@ -99,3 +102,166 @@ def create_limited_print_func(print_func, max_page_size):
raise PrintLimitReached()
return fn
+
+
+class Formatter(string.Formatter):
+ def arg_of_field_name(self, field_name):
+ return _string.formatter_field_name_split(field_name)[0]
+
+
+class FormatPlaceholder(object):
+ def __init__(self, ident):
+ self.ident = str(ident)
+ def __repr__(self):
+ return "%s(%r)" % (self.__class__.__name__, self.ident)
+ def __format__(self, spec):
+ result = self.ident
+ if spec:
+ result += ":" + spec
+ return "{" + result + "}"
+ def __getitem__(self, key):
+ return FormatPlaceholder(self.ident + "[" + str(key) + "]")
+ def __getattr__(self, attr):
+ return FormatPlaceholder(self.ident + "." + str(attr))
+
+
+class PartialString(object):
+ """A format string where the "holes" are indexed by arbitrary python
+ objects instead of string names or integer indexes. This is useful when you
+ need to compose these objects together, but don't want users of the partial
+ string to have to deal with artificial "indexes" for the holes.
+
+ For example:
+
+ >>> a, b = object(), object()
+ >>> tmpl = PartialString("{0} {1}", a, b)
+ >>> tmpl
+ PartialString('{0} {1}', <object object at ...>, <object object at ...>)
+ >>> tmpl.holes == (a, b)
+ True
+ >>> tmpl.format({a: "Hello,", b: "World!"})
+ 'Hello, World!'
+
+ You can partially fill up the holes:
+
+ >>> tmpl.pformat({a: "Hello,"}) == PartialString('Hello, {0}', b)
+ True
+ >>> tmpl.pformat({b: "World!"}) == PartialString('{0} World!', a)
+ True
+
+ You can estimate the size of the filled-up string:
+
+ >>> tmpl.base_len, tmpl.num_holes
+ (1, 2)
+ >>> tmpl.size(hole_size=33)
+ 67
+
+ You can also partially fill up the holes using more PartialStrings,
+ even recursively:
+
+ >>> tmpl.pformat({a: PartialString('{0}', b)}) == PartialString('{0} {0}', b)
+ True
+ >>> tmpl.pformat({a: tmpl}) == PartialString('{0} {1} {1}', a, b)
+ True
+ >>> tmpl.pformat({b: tmpl}) == PartialString('{0} {0} {1}', a, b)
+ True
+
+ Finally, the holes have to match what's in the format string:
+
+ >>> tmpl = PartialString("{0} {1} {2}", a, b)
+ Traceback (most recent call last):
+ ...
+ IndexError: tuple index out of range
+
+ CAVEATS:
+
+ Filling up holes using other PartialStrings, does not play very nicely with
+ format specifiers. For example:
+
+ >>> tmpl = PartialString("{0:20} {1.child}", a, b)
+ >>> tmpl.pformat({a: tmpl})
+ PartialString('{0:20} {1.child} {1.child}', <object ...>, <object ...>)
+ >>> tmpl.pformat({b: tmpl})
+ Traceback (most recent call last):
+ ...
+ AttributeError: ... has no attribute 'child'
+
+ So you probably want to avoid such usages. The exact behaviour of these
+ might change in the future, too.
+ """
+ formatter = Formatter()
+
+ def __init__(self, fmtstr="", *holes):
+ # Ensure the format string is valid, and figure out some basic stats
+ fmt = self.formatter
+ pieces = [(len(l), f) for l, f, _, _ in fmt.parse(fmtstr)]
+ used_args = set(fmt.arg_of_field_name(f) for _, f in pieces if f is not None)
+ self.num_holes = sum(1 for _, f in pieces if f is not None)
+ self.base_len = sum(l for l, _ in pieces)
+
+ # Remove unused and duplicates in the holes objects
+ seen = collections.OrderedDict()
+ mapping = tuple(FormatPlaceholder(seen.setdefault(k, len(seen))) if i in used_args else None
+ for i, k in enumerate(holes))
+ self._fmtstr = fmt.vformat(fmtstr, mapping, {})
+ self.holes = tuple(seen.keys())
+
+ def __eq__(self, other):
+ return (self is other or isinstance(other, PartialString) and
+ other._fmtstr == self._fmtstr and
+ other.holes == self.holes)
+
+ def __repr__(self):
+ return "%s%r" % (self.__class__.__name__, (self._fmtstr,) + self.holes)
+
+ def _offset_fmtstr(self, offset):
+ return self._fmtstr.format(*(FormatPlaceholder(i + offset) for i in range(len(self.holes))))
+
+ def _pformat(self, mapping):
+ new_holes = []
+ real_mapping = []
+ for i, k in enumerate(self.holes):
+ if k in mapping:
+ v = mapping[k]
+ if isinstance(v, PartialString):
+ out = v._offset_fmtstr(len(new_holes))
+ new_holes.extend(v.holes)
+ else:
+ out = v
+ else:
+ out = FormatPlaceholder(len(new_holes))
+ new_holes.append(k)
+ real_mapping.append(out)
+ return self._fmtstr.format(*real_mapping), new_holes
+
+ def size(self, hole_size=1):
+ return self.base_len + hole_size * self.num_holes
+
+ def pformat(self, mapping):
+ """Partially apply a mapping, returning a new PartialString."""
+ new_fmtstr, new_holes = self._pformat(mapping)
+ return self.__class__(new_fmtstr, *new_holes)
+
+ def format(self, mapping):
+ """Fully apply a mapping, returning a string."""
+ new_fmtstr, new_holes = self._pformat(mapping)
+ if new_holes:
+ raise ValueError("not all holes filled: %r" % new_holes)
+ return new_fmtstr
+
+ @classmethod
+ def of(cls, obj):
+ """Create a partial string for a single object.
+
+ >>> e = PartialString.of(None)
+ >>> e.pformat({None: e}) == e
+ True
+ """
+ return cls("{0}", obj)
+
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
+ a, b = object(), object()
+ tmpl = PartialString("{0} {1}", a, b)
diff --git a/tests/test_presenters.py b/tests/test_presenters.py
index 538c01c..3e60c78 100644
--- a/tests/test_presenters.py
+++ b/tests/test_presenters.py
@@ -22,7 +22,7 @@ import re
import pytest
from diffoscope.main import main
-from diffoscope.presenters.utils import create_limited_print_func, PrintLimitReached
+from diffoscope.presenters.utils import create_limited_print_func, PrintLimitReached, PartialString
from .utils.data import cwd_data, get_data
@@ -152,3 +152,19 @@ def test_limited_print():
p = create_limited_print_func(fake, 5)
p("123")
p("456", force=True)
+
+def test_partial_string():
+ a, b = object(), object()
+ tmpl = PartialString("{0} {1}", a, b)
+ assert tmpl.holes == (a, b)
+ assert tmpl.format({a: "Hello,", b: "World!"}) == 'Hello, World!'
+ assert tmpl.pformat({a: "Hello,"}) == PartialString('Hello, {0}', b)
+ assert tmpl.pformat({b: "World!"}) == PartialString('{0} World!', a)
+ assert tmpl.base_len, tmpl.num_holes == (1, 2)
+ assert tmpl.size(hole_size=33) == 67
+ assert tmpl.pformat({a: PartialString('{0}', b)}) == PartialString('{0} {0}', b)
+ assert tmpl.pformat({a: tmpl}) == PartialString('{0} {1} {1}', a, b)
+ assert tmpl.pformat({b: tmpl}) == PartialString('{0} {0} {1}', a, b)
+ PartialString("{1}", a, b)
+ with pytest.raises(IndexError):
+ PartialString("{0} {1} {2}", a, b)
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/diffoscope.git
More information about the diffoscope
mailing list