[diffoscope] 03/05: presenters: html: more intuitive "limit" flags, some backwards-incompatible changes:
Ximin Luo
infinity0 at debian.org
Mon Jun 26 19:32:38 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 ac98f8595d76fec9f8fd6eaa49eb4128c0799d2d
Author: Ximin Luo <infinity0 at debian.org>
Date: Mon Jun 26 16:00:18 2017 +0200
presenters: html: more intuitive "limit" flags, some backwards-incompatible changes:
--max-report-size:
Old: in --html-dir this limited only the parent page
New: in --html-dir this applies across all pages
--max-diff-block-lines:
Old: in --html-dir 4 * this number applied across all pages (for a given diff block)
New: in --html-dir this applies across all pages (for a given diff block)
--max-page-size:
New flag
Applies to the sole --html page, or the top-level --html-dir page
--max-report-child-size
Renamed to
--max-page-size-child:
No behavioural changes
--max-diff-block-lines-parent
Renamed to
--max-page-diff-block-lines:
Old: Only applied to the top-level --html-dir page
New: Applies to the sole --html page, or the top-level --html-dir page
The reasoning behind these changes is that it's unlikely someone would want to
generate a 500MB single html page, but they might in theory generate a 500MB html
directory split up into several 200KB pages, plus a single 200KB html page as
a summary. The new semantics for these flags allows both to be generated in one
run using the same set of flags.
---
diffoscope/config.py | 39 ++--
diffoscope/main.py | 108 ++++++-----
diffoscope/presenters/html/html.py | 319 +++++++++++++++++++++++---------
diffoscope/presenters/html/templates.py | 16 +-
diffoscope/presenters/utils.py | 22 ++-
5 files changed, 340 insertions(+), 164 deletions(-)
diff --git a/diffoscope/config.py b/diffoscope/config.py
index bd6b3a9..7d1e372 100644
--- a/diffoscope/config.py
+++ b/diffoscope/config.py
@@ -20,16 +20,21 @@
class Config(object):
- max_diff_block_lines = 256
- max_diff_block_lines_parent = 50
- max_diff_block_lines_saved = float("inf")
- # html-dir output uses ratio * max-diff-block-lines as its limit
- max_diff_block_lines_html_dir_ratio = 4
# GNU diff cannot process arbitrary large files :(
- max_diff_input_lines = 2 ** 20
- max_report_size = 2000 * 2 ** 10 # 2000 kB
+ max_diff_input_lines = 2 ** 22
+ max_diff_block_lines_saved = float("inf")
+
+ # hard limits, restricts single-file and multi-file formats
+ max_report_size = 40 * 2 ** 20 # 40 MB
+ max_diff_block_lines = 2 ** 10 # 1024 lines
+ # structural limits, restricts single-file formats
+ # semi-restricts multi-file formats
+ max_page_size = 400 * 2 ** 10 # 400 kB
+ max_page_size_child = 200 * 2 ** 10 # 200 kB
+ max_page_diff_block_lines = 2 ** 7 # 128 lines
+
max_text_report_size = 0
- max_report_child_size = 500 * 2 ** 10
+
new_file = False
fuzzy_threshold = 60
enforce_constraints = True
@@ -46,15 +51,13 @@ class Config(object):
def __setattr__(self, k, v):
super(Config, self).__setattr__(k, v)
- if self.enforce_constraints:
- self.check_constraints()
+ def check_ge(self, a, b):
+ va = getattr(self, a)
+ vb = getattr(self, b)
+ if va < vb:
+ raise ValueError("{0} ({1}) cannot be smaller than {2} ({3})".format(a, va, b, vb))
def check_constraints(self):
- max_ = self.max_diff_block_lines_html_dir_ratio * \
- self.max_diff_block_lines
- if self.max_diff_block_lines_saved < max_: # noqa
- raise ValueError("max_diff_block_lines_saved "
- "({0.max_diff_block_lines_saved}) cannot be smaller than "
- "{0.max_diff_block_lines_html_dir_ratio} * "
- "max_diff_block_lines ({1})".format(self, max_),
- )
+ self.check_ge("max_diff_block_lines", "max_page_diff_block_lines")
+ self.check_ge("max_report_size", "max_page_size")
+ self.check_ge("max_report_size", "max_page_size_child")
diff --git a/diffoscope/main.py b/diffoscope/main.py
index 504a8fc..32bdf4d 100644
--- a/diffoscope/main.py
+++ b/diffoscope/main.py
@@ -78,6 +78,8 @@ def create_parser():
const=True, help='Show an approximate progress bar')
parser.add_argument('--no-progress', dest='progress', action='store_const',
const=False, help='Do not show any progress bar')
+ parser.add_argument('--no-default-limits', action='store_true', default=False,
+ help='Disable most default output limits and diff calculation limits.')
group1 = parser.add_argument_group('output types')
group1.add_argument('--text', metavar='OUTPUT_FILE', dest='text_output',
@@ -113,49 +115,44 @@ def create_parser():
help='Write profiling info to given file (use - for stdout)')
group2 = parser.add_argument_group('output limits')
- group2.add_argument('--no-default-limits', action='store_true', default=False,
- help='Disable most default limits. Note that text '
- 'output already ignores most of these.')
+ # everything marked with default=None below is affected by no-default-limits
group2.add_argument('--max-text-report-size', metavar='BYTES', type=int,
help='Maximum bytes written in --text report. (0 to '
- 'disable)', default=None).completer=RangeCompleter(0,
- Config().max_text_report_size, 200000)
+ 'disable, default: %d)' % Config().max_text_report_size,
+ default=None)
group2.add_argument('--max-report-size', metavar='BYTES', type=int,
- help='Maximum bytes written in report. In html-dir '
- 'output, this is the max bytes of the parent page. '
- '(0 to disable, default: %d)' %
- Config().max_report_size,
- default=None).completer=RangeCompleter(0,
- Config().max_report_size, 200000)
- group2.add_argument('--max-report-child-size', metavar='BYTES', type=int,
- help='In --html-dir output, this is the max bytes of '
- 'each child page (0 to disable, default: %(default)s, '
- 'remaining in effect even with --no-default-limits)',
- default=Config().max_report_child_size).completer=RangeCompleter(0,
- Config().max_report_child_size, 50000)
+ help='Maximum bytes of a report in a given format, '
+ 'across all of its pages. Note that some formats, such '
+ 'as --html, may be restricted by even smaller limits '
+ 'such as --max-page-size. (0 to disable, default: %d)' %
+ Config().max_report_size, default=None).completer=RangeCompleter(
+ Config().max_report_size)
group2.add_argument('--max-diff-block-lines', metavar='LINES', type=int,
- help='Maximum number of lines output per diff block. '
- 'In --html-dir output, we use %d times this number instead, '
- 'taken over all pages. (0 to disable, default: %d)' %
- (Config().max_diff_block_lines_html_dir_ratio,
- Config().max_diff_block_lines),
- default=None).completer=RangeCompleter(0,
- Config().max_diff_block_lines, 5)
- group2.add_argument('--max-diff-block-lines-parent', metavar='LINES', type=int,
- help='In --html-dir output, this is maximum number of '
- 'lines output per diff block on the parent page '
- 'before spilling it into child pages (0 to disable, '
- 'default: %(default)s, remaining in effect even with '
- '--no-default-limits)',
- default=Config().max_diff_block_lines_parent).completer=RangeCompleter(0,
- Config().max_diff_block_lines_parent, 200)
- group2.add_argument('--max-diff-block-lines-saved', metavar='LINES', type=int,
- help='Maximum number of lines saved per diff block. '
- 'Most users should not need this, unless you run out '
- 'of memory. This truncates diff(1) output before even '
- 'trying to emit it in a report. This also affects --text '
- 'output. (0 to disable, default: 0)',
- default=0).completer=RangeCompleter(0, 0, 200)
+ help='Maximum number of lines output per unified-diff '
+ 'block, across all pages. (0 to disable, default: %d)' %
+ Config().max_diff_block_lines, default=None).completer=RangeCompleter(
+ Config().max_diff_block_lines)
+ group2.add_argument('--max-page-size', metavar='BYTES', type=int,
+ help='Maximum bytes of the top-level (--html-dir) or sole '
+ '(--html) page. (default: %(default)s, remains in effect '
+ 'even with --no-default-limits)', default=
+ Config().max_page_size).completer=RangeCompleter(
+ Config().max_page_size)
+ group2.add_argument('--max-page-size-child', metavar='BYTES', type=int,
+ help='In --html-dir output, this is the maximum bytes of '
+ 'each child page (default: %(default)s, remains in '
+ 'effect even with --no-default-limits)', default=
+ Config().max_page_size_child).completer=RangeCompleter(
+ Config().max_page_size_child)
+ group2.add_argument('--max-page-diff-block-lines', metavar='LINES', type=int,
+ help='Maximum number of lines output per unified-diff block '
+ 'on the top-level (--html-dir) or sole (--html) page, before '
+ 'spilling it into child pages (--html-dir) or skipping the '
+ 'rest of the diff block. Child pages are limited instead by '
+ '--max-page-size-child. (default: %(default)s, remains in '
+ 'effect even with --no-default-limits)', default=
+ Config().max_page_diff_block_lines).completer=RangeCompleter(
+ Config().max_page_diff_block_lines)
group3 = parser.add_argument_group('diff calculation')
group3.add_argument('--new-file', action='store_true',
@@ -173,19 +170,25 @@ def create_parser():
group3.add_argument('--fuzzy-threshold', type=int,
help='Threshold for fuzzy-matching '
'(0 to disable, %(default)s is default, 400 is high fuzziness)',
- default=Config().fuzzy_threshold).completer=RangeCompleter(0,
- 400, 20)
+ default=Config().fuzzy_threshold).completer=RangeCompleter(400)
group3.add_argument('--max-diff-input-lines', metavar='LINES', type=int,
help='Maximum number of lines fed to diff(1) '
'(0 to disable, default: %d)' %
Config().max_diff_input_lines,
- default=None).completer=RangeCompleter(0,
- Config().max_diff_input_lines, 5000)
+ default=None).completer=RangeCompleter(
+ Config().max_diff_input_lines)
group3.add_argument('--max-container-depth', metavar='DEPTH', type=int,
help='Maximum depth to recurse into containers. '
'(Cannot be disabled for security reasons, default: '
'%(default)s)',
default=Config().max_container_depth)
+ group3.add_argument('--max-diff-block-lines-saved', metavar='LINES', type=int,
+ help='Maximum number of lines saved per diff block. '
+ 'Most users should not need this, unless you run out '
+ 'of memory. This truncates diff(1) output before emitting '
+ 'it in a report, and affects all types of output, '
+ 'including --text and --json. (0 to disable, default: '
+ '%(default)s)', default=0)
group4 = parser.add_argument_group('information commands')
group4.add_argument('--help', '-h', action='help',
@@ -214,8 +217,12 @@ def create_parser():
class RangeCompleter(object):
- def __init__(self, start, end, step):
- self.choices = range(start, end + 1, step)
+ def __init__(self, start, end=0, divisions=16):
+ if end < start:
+ tmp = end
+ end = start
+ start = tmp
+ self.choices = range(start, end + 1, int((end-start+1)/divisions))
def __call__(self, prefix, **kwargs):
return (str(i) for i in self.choices if str(i).startswith(prefix))
@@ -272,6 +279,7 @@ class ListDebianSubstvarsAction(argparse._StoreTrueAction):
sys.exit(0)
def maybe_set_limit(config, parsed_args, key):
+ # apply limits affected by "no-default-limits"
v = getattr(parsed_args, key)
if v is not None:
setattr(config, key, float("inf") if v == 0 else v)
@@ -289,11 +297,12 @@ def run_diffoscope(parsed_args):
logger.warning('Fuzzy-matching is currently disabled as the "tlsh" module is unavailable.')
maybe_set_limit(Config(), parsed_args, "max_report_size")
maybe_set_limit(Config(), parsed_args, "max_text_report_size")
- maybe_set_limit(Config(), parsed_args, "max_report_child_size")
- # need to set them in this order due to Config._check_constraints
- maybe_set_limit(Config(), parsed_args, "max_diff_block_lines_saved")
- maybe_set_limit(Config(), parsed_args, "max_diff_block_lines_parent")
maybe_set_limit(Config(), parsed_args, "max_diff_block_lines")
+ Config().max_page_size = parsed_args.max_page_size
+ Config().max_page_size_child = parsed_args.max_page_size_child
+ Config().max_page_diff_block_lines = parsed_args.max_page_diff_block_lines
+
+ maybe_set_limit(Config(), parsed_args, "max_diff_block_lines_saved")
maybe_set_limit(Config(), parsed_args, "max_diff_input_lines")
Config().max_container_depth = parsed_args.max_container_depth
Config().fuzzy_threshold = parsed_args.fuzzy_threshold
@@ -301,6 +310,7 @@ def run_diffoscope(parsed_args):
Config().excludes = parsed_args.excludes
Config().exclude_commands = parsed_args.exclude_commands
Config().compute_visual_diffs = PresenterManager().compute_visual_diffs()
+ Config().check_constraints()
set_path()
set_locale()
path1, path2 = parsed_args.path1, parsed_args.path2
diff --git a/diffoscope/presenters/html/html.py b/diffoscope/presenters/html/html.py
index 1648a36..4d896dc 100644
--- a/diffoscope/presenters/html/html.py
+++ b/diffoscope/presenters/html/html.py
@@ -71,6 +71,20 @@ re_anchor_prefix = re.compile(r'^[^A-Za-z]')
re_anchor_suffix = re.compile(r'[^A-Za-z-_:\.]')
+def send_and_exhaust(iterator, arg, default):
+ """Send a single value to a coroutine, exhaust it, and return the final
+ element or a default value if it was empty."""
+ # Python's coroutine syntax is still a bit rough when you want to do
+ # slightly more complex stuff. Watch this logic closely.
+ output = default
+ try:
+ output = iterator.send(arg)
+ except StopIteration:
+ pass
+ for output in iterator:
+ pass
+ return output
+
def convert(s, ponct=0, tag=''):
i = 0
t = io.StringIO()
@@ -160,48 +174,65 @@ def output_node_frame(difference, path, indentstr, indentnum, body):
html.escape(PartialString.escape(difference.source1)),
html.escape(PartialString.escape(difference.source2)))
- return u"""{0[1]}<div class="diffheader">
+ return PartialString.numl(u"""{0[1]}<div class="diffheader">
{1}{0[1]}</div>
-{2}""".format(indent, header, body)
+{2}""", 3).pformatl(indent, header, body)
def output_node(difference, path, indentstr, indentnum, css_url, directory):
+ """Returns a tuple (parent, continuation) where
+
+ - parent is a PartialString representing the body of the node, including
+ its comments, visuals, unified_diff and headers for its children - but
+ not the bodies of the children
+ - continuation is either None or (only in html-dir mode) a function which
+ when called with a single integer arg, the maximum size to print, will
+ print any remaining "split" pages for unified_diff up to the given size.
+ """
indent = tuple(indentstr * (indentnum + x) for x in range(3))
t, cont = PartialString.cont()
+ comments = u""
if difference.comments:
comments = u'{0[1]}<div class="comment">\n{1}{0[1]}</div>\n'.format(
indent, "".join(u"{0[2]}{1}<br/>\n".format(indent, html.escape(x)) for x in difference.comments))
- else:
- comments = u""
visuals = u""
for visual in difference.visuals:
visuals += output_visual(visual, output_anchor(path), indentstr, indentnum+1)
- udiff = io.StringIO()
+ udiff = u""
+ ud_cont = None
if difference.unified_diff:
- def print_func(x, force=False):
- udiff.write(x)
- HTMLPresenter().output_unified_diff(print_func, css_url, directory, difference.unified_diff, difference.has_internal_linenos)
+ ud_cont = HTMLSideBySidePresenter().output_unified_diff(
+ css_url, directory, difference.unified_diff,
+ difference.has_internal_linenos)
+ udiff = next(ud_cont)
+ if isinstance(udiff, PartialString):
+ ud_cont = ud_cont.send
+ udiff = udiff.pformatl(PartialString.of(ud_cont))
+ else:
+ for _ in ud_cont: pass # exhaust the iterator, avoids GeneratorExit
+ udiff = t.escape(udiff)
+ ud_cont = None
- # Construct a PartialString for this node
- # {3} gets mapped to {-1}, a continuation hole for later child nodes
- body = u"{0}{1}{2}{3}".format(t.escape(comments), t.escape(visuals), t.escape(udiff.getvalue()), "{-1}")
+ # PartialString for this node
+ body = PartialString.numl(u"{0}{1}{2}{-1}", 3, cont).pformatl(
+ t.escape(comments), t.escape(visuals), udiff)
if len(path) == 1:
# root node, frame it
- t = cont(t, output_node_frame(difference, path, indentstr, indentnum, body))
- else:
- t = cont(t, body)
+ body = output_node_frame(difference, path, indentstr, indentnum, body)
+ t = cont(t, body)
# Add holes for child nodes
for d in difference.details:
- # {0} hole, for the child node's contents
- # {-1} continuation hole, for later child nodes
- t = cont(t, u"""{0[1]}<div class="difference">
+ child = output_node_frame(d, path + [d], indentstr, indentnum+1, PartialString.of(d))
+ child = PartialString.numl(u"""{0[1]}<div class="difference">
{1}{0[1]}</div>
-{{-1}}""".format(indent, output_node_frame(d, path + [d], indentstr, indentnum+1, "{0}")), d)
+{-1}""", 2, cont).pformatl(indent, child)
+ t = cont(t, child)
- return cont(t, u"")
+ assert len(t.holes) >= len(difference.details) + 1 # there might be extra holes for the unified diff continuation
+ return cont(t, u""), ud_cont
def output_header(css_url):
if css_url:
@@ -226,32 +257,39 @@ def file_printer(directory, filename):
yield f.write
@contextlib.contextmanager
-def spl_file_printer(directory, filename):
+def spl_file_printer(directory, filename, accum):
with codecs.open(os.path.join(directory,filename), 'w', encoding='utf-8') as f:
print_func = f.write
- def recording_print_func(s, force=False):
+ def recording_print_func(s):
print_func(s)
recording_print_func.bytes_written += len(s)
+ accum.bytes_written += len(s)
recording_print_func.bytes_written = 0
yield recording_print_func
-class HTMLPresenter(Presenter):
+class HTMLSideBySidePresenter(object):
supports_visual_diffs = True
def __init__(self):
- self.new_unified_diff()
+ self.max_lines = Config().max_diff_block_lines # only for html-dir
+ self.max_lines_parent = Config().max_page_diff_block_lines
+ self.max_page_size_child = Config().max_page_size_child
def new_unified_diff(self):
self.spl_rows = 0
self.spl_current_page = 0
self.spl_print_func = None
self.spl_print_ctrl = None
+ # the below apply to child pages only, the parent page limit works
+ # differently and is controlled by output_difference later below
+ self.bytes_max_total = 0
+ self.bytes_written = 0
+ self.error_row = None
def output_hunk_header(self, hunk_off1, hunk_size1, hunk_off2, hunk_size2):
self.spl_print_func(u'<tr class="diffhunk"><td colspan="2">Offset %d, %d lines modified</td>' % (hunk_off1, hunk_size1))
self.spl_print_func(u'<td colspan="2">Offset %d, %d lines modified</td></tr>\n' % (hunk_off2, hunk_size2))
- self.row_was_output()
def output_line(self, has_internal_linenos, type_name, s1, line1, s2, line2):
if s1 and len(s1) > MAX_LINE_SIZE:
@@ -283,8 +321,7 @@ class HTMLPresenter(Presenter):
else:
self.spl_print_func(u'<td colspan="2">\xa0</td>')
finally:
- self.spl_print_func(u"</tr>\n", force=True)
- self.row_was_output()
+ self.spl_print_func(u"</tr>\n")
def spl_print_enter(self, print_context, rotation_params):
# Takes ownership of print_context
@@ -299,54 +336,71 @@ class HTMLPresenter(Presenter):
def spl_print_exit(self, *exc_info):
if not self.spl_had_entered_child(): return False
- self.spl_print_func(output_footer(), force=True)
+ self.spl_print_func(output_footer())
_exit, _ = self.spl_print_ctrl
self.spl_print_func = None
self.spl_print_ctrl = None
return _exit(*exc_info)
- def row_was_output(self):
- self.spl_rows += 1
- _, rotation_params = self.spl_print_ctrl
- max_lines = Config().max_diff_block_lines
- max_lines_parent = Config().max_diff_block_lines_parent
- max_lines_ratio = Config().max_diff_block_lines_html_dir_ratio
- max_report_child_size = Config().max_report_child_size
- if not rotation_params:
+ def check_limits(self):
+ if not self.spl_print_ctrl[1]:
# html-dir single output, don't need to rotate
- if self.spl_rows >= max_lines:
+ if self.spl_rows >= self.max_lines_parent:
raise DiffBlockLimitReached()
- return
+ return False
else:
# html-dir output, perhaps need to rotate
- directory, mainname, css_url = rotation_params
- if self.spl_rows >= max_lines_ratio * max_lines:
+ if self.spl_rows >= self.max_lines:
raise DiffBlockLimitReached()
if self.spl_current_page == 0: # on parent page
- if self.spl_rows < max_lines_parent:
- return
+ if self.spl_rows < self.max_lines_parent:
+ return False
+ logger.debug("new unified-diff subpage, parent page went over %s lines", self.max_lines_parent)
else: # on child page
- # TODO: make this stay below the max, instead of going 1 row over the max
- # will require some backtracking...
- if self.spl_print_func.bytes_written < max_report_child_size:
- return
+ if self.bytes_max_total and self.bytes_written > self.bytes_max_total:
+ raise PrintLimitReached()
+ if self.spl_print_func.bytes_written < self.max_page_size_child:
+ return False
+ logger.debug("new unified-diff subpage, previous subpage went over %s bytes", self.max_page_size_child)
+ return True
+ def new_child_page(self):
+ _, rotation_params = self.spl_print_ctrl
+ directory, mainname, css_url = rotation_params
self.spl_current_page += 1
filename = "%s-%s.html" % (mainname, self.spl_current_page)
if self.spl_current_page > 1:
# previous page was a child, close it
- self.spl_print_func(templates.UD_TABLE_FOOTER % {"filename": html.escape(filename), "text": "load diff"}, force=True)
+ self.spl_print_func(templates.UD_TABLE_FOOTER % {"filename": html.escape(filename), "text": "load diff"})
+ self.spl_print_func(u"</table>\n")
self.spl_print_exit(None, None, None)
# rotate to the next child page
- context = spl_file_printer(directory, filename)
+ context = spl_file_printer(directory, filename, self)
self.spl_print_enter(context, rotation_params)
self.spl_print_func(templates.UD_TABLE_HEADER)
+ def output_limit_reached(self, limit_type, total, bytes_processed):
+ logger.debug('%s print limit reached', limit_type)
+ bytes_left = total - bytes_processed
+ self.error_row = templates.UD_TABLE_LIMIT_FOOTER % {
+ "limit_type": limit_type,
+ "bytes_left": bytes_left,
+ "bytes_total": total,
+ "percent": (bytes_left / total) * 100
+ }
+ self.spl_print_func(self.error_row)
+
def output_unified_diff_table(self, unified_diff, has_internal_linenos):
- self.spl_print_func(templates.UD_TABLE_HEADER)
+ """Output a unified diff <table> possibly over multiple pages.
+
+ It is the caller's responsibility to set up self.spl_* correctly.
+
+ Yields None for each extra child page, and then True or False depending
+ on whether the whole output was truncated.
+ """
try:
ydiff = SideBySideDiff(unified_diff)
for t, args in ydiff.items():
@@ -358,67 +412,135 @@ class HTMLPresenter(Presenter):
self.spl_print_func(u'<td colspan="2">%s</td>\n' % args)
else:
raise AssertionError()
- return True
+ self.spl_rows += 1
+ if not self.check_limits():
+ continue
+ self.new_child_page()
+ new_limit = yield None
+ if new_limit:
+ self.bytes_max_total = new_limit
+ self.bytes_written = 0
+ self.check_limits()
+ wrote_all = True
+ except GeneratorExit:
+ return
except DiffBlockLimitReached:
- total = len(unified_diff)
- bytes_left = total - ydiff.bytes_processed
- frac = bytes_left / total
- self.spl_print_func(
- u'<tr class="error">'
- u'<td colspan="4">Max diff block lines reached; %s/%s bytes (%.2f%%) of diff not shown.'
- u"</td></tr>" % (bytes_left, total, frac*100), force=True)
- logger.debug('diff-block print limit reached')
- return False
+ self.output_limit_reached("diff block lines", len(unified_diff), ydiff.bytes_processed)
+ wrote_all = False
except PrintLimitReached:
- assert not self.spl_had_entered_child() # limit reached on the parent page
- self.spl_print_func(u'<tr class="error"><td colspan="4">Max output size reached.</td></tr>', force=True)
- raise
+ self.output_limit_reached("report size", len(unified_diff), ydiff.bytes_processed)
+ wrote_all = False
finally:
- self.spl_print_func(u"</table>", force=True)
+ # no footer on the last page, just a close tag
+ self.spl_print_func(u"</table>")
+ yield wrote_all
- def output_unified_diff(self, print_func, css_url, directory, unified_diff, has_internal_linenos):
+ def output_unified_diff(self, css_url, directory, unified_diff, has_internal_linenos):
self.new_unified_diff()
rotation_params = None
if directory:
mainname = hashlib.md5(unified_diff.encode('utf-8')).hexdigest()
rotation_params = directory, mainname, css_url
+
try:
- self.spl_print_func = print_func
+ udiff = io.StringIO()
+ udiff.write(templates.UD_TABLE_HEADER)
+ self.spl_print_func = udiff.write
self.spl_print_ctrl = None, rotation_params
- truncated = not self.output_unified_diff_table(unified_diff, has_internal_linenos)
+
+ it = self.output_unified_diff_table(unified_diff, has_internal_linenos)
+ wrote_all = next(it)
+ if wrote_all is None:
+ assert self.spl_current_page == 1
+ # now pause the iteration and wait for consumer to give us a
+ # size-limit to write the remaining pages with
+ # exhaust the iterator and save the last item in wrote_all
+ new_limit = yield PartialString(PartialString.escape(udiff.getvalue()) + u"{0}</table>\n", None)
+ wrote_all = send_and_exhaust(it, new_limit, wrote_all)
+ else:
+ yield udiff.getvalue()
+ return
+
+ except GeneratorExit:
+ logger.debug("skip extra output for unified diff %s", mainname)
+ it.close()
+ self.spl_print_exit(None, None, None)
+ return
except:
- if not self.spl_print_exit(*sys.exc_info()): raise
+ import traceback
+ traceback.print_exc()
+ if self.spl_print_exit(*sys.exc_info()) is False: raise
else:
self.spl_print_exit(None, None, None)
finally:
self.spl_print_ctrl = None
self.spl_print_func = None
- if self.spl_current_page > 0:
+ truncated = not wrote_all
+ child_rows_written = self.spl_rows - self.max_lines_parent
+ if truncated and not child_rows_written:
+ # if we didn't write any child rows, just output the error message on the parent page
+ parent_last_row = self.error_row
+ else:
noun = "pieces" if self.spl_current_page > 1 else "piece"
text = "load diff (%s %s%s)" % (self.spl_current_page, noun, (", truncated" if truncated else ""))
- print_func(templates.UD_TABLE_FOOTER % {"filename": html.escape("%s-1.html" % mainname), "text": text}, force=True)
+ parent_last_row = templates.UD_TABLE_FOOTER % {"filename": html.escape("%s-1.html" % mainname), "text": text}
+ yield self.bytes_written, parent_last_row
+
+
+class HTMLPresenter(Presenter):
+ supports_visual_diffs = True
+
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.report_printed = 0
+ self.report_limit = Config().max_report_size
+
+ @property
+ def report_remaining(self):
+ return self.report_limit - self.report_printed
+
+ def maybe_print(self, node, printers, outputs, continuations):
+ output = outputs[node]
+ node_cont = continuations[node]
+ if output.holes and set(output.holes) - set(node_cont):
+ return
+
+ # could be slightly more accurate, whatever
+ est_placeholder_len = max(len(templates.UD_TABLE_FOOTER), len(templates.UD_TABLE_LIMIT_FOOTER)) + 40
+ est_size = output.size(est_placeholder_len)
+
+ results = {}
+ for cont in node_cont:
+ remaining = self.report_remaining - est_size
+ printed, result = cont(remaining)
+ self.report_printed += printed
+ results[cont] = result
+
+ out = output.format(results)
+ printer_args = printers[node]
+ with printer_args[0](*printer_args[1:]) as printer:
+ printer(out)
+ self.report_printed += len(out)
+
+ del outputs[node]
+ del printers[node]
+ del continuations[node]
def output_node_placeholder(self, anchor, lazy_load):
if lazy_load:
return templates.DIFFNODE_LAZY_LOAD % anchor
else:
- return '<div class="error">Max report size reached</div>\n'
+ return templates.DIFFNODE_LIMIT
def output_difference(self, target, difference, css_url, jquery_url, single_page=False):
outputs = {} # nodes to their partial output
ancestors = {} # child nodes to ancestor nodes
placeholder_len = len(self.output_node_placeholder("XXXXXXXXXXXXXXXX", not single_page))
-
+ continuations = {} # functions to print unified diff continuations (html-dir only)
printers = {} # nodes to their printers
- def maybe_print(node):
- if outputs[node].holes:
- return
- printer_args = printers[node]
- with printer_args[0](*printer_args[1:]) as printer:
- printer(outputs[node].format())
- del outputs[node]
- del printers[node]
def smallest_first(node, parscore):
depth = parscore[0] + 1 if parscore else 0
@@ -431,34 +553,44 @@ class HTMLPresenter(Presenter):
path = score[3]
anchor = output_anchor(path)
logger.debug('html output for %s', anchor)
- node_output = output_node(node, path, " ", len(path)-1, css_url, None if single_page else target)
+ node_output, node_continuation = output_node(
+ node, path, " ", len(path)-1, css_url, None if single_page else target)
+ add_to_existing = False
if ancestor:
- limit = Config().max_report_child_size
- logger.debug("output size: %s, %s",
- outputs[ancestor].size(placeholder_len), node_output.size(placeholder_len))
- else:
- limit = Config().max_report_size
+ page_limit = Config().max_page_size if ancestor is difference else Config().max_page_size_child
+ page_current = outputs[ancestor].size(placeholder_len)
+ report_current = self.report_printed + sum(p.size(placeholder_len) for p in outputs.values())
+ want_to_add = node_output.size(placeholder_len)
+ logger.debug("report size: %s/%s, page size: %s/%s, want to add %s)", report_current, self.report_limit, page_current, page_limit, want_to_add)
+ if report_current + want_to_add > self.report_limit:
+ make_new_subpage = False
+ elif page_current + want_to_add < page_limit:
+ add_to_existing = True
+ else:
+ make_new_subpage = not single_page
- if ancestor and outputs[ancestor].size(placeholder_len) + node_output.size(placeholder_len) < limit:
+ if add_to_existing:
# under limit, add it to an existing page
outputs[ancestor] = outputs[ancestor].pformat({node: node_output})
stored = ancestor
else:
- # over limit (or root), new subpage
+ # over limit (or root), new subpage or continue/break
if ancestor:
- placeholder = self.output_node_placeholder(anchor, not single_page)
+ placeholder = self.output_node_placeholder(anchor, make_new_subpage)
outputs[ancestor] = outputs[ancestor].pformat({node: placeholder})
- maybe_print(ancestor)
+ self.maybe_print(ancestor, printers, outputs, continuations)
footer = output_footer()
- if single_page:
+ if not make_new_subpage: # we hit a limit, either max-report-size or single-page
if not outputs:
- # already output a single page, don't iterate through any more children
+ # no more holes, don't iterate through any more children
break
else:
+ # more holes to fill up with "limit reached" placeholders
continue
else:
+ # unconditionally write the root node regardless of limits
assert node is difference
footer = output_footer(jquery_url)
anchor = "index"
@@ -466,13 +598,18 @@ class HTMLPresenter(Presenter):
outputs[node] = node_output.frame(
output_header(css_url) + u'<div class="difference">\n',
u'</div>\n' + footer)
+ assert not single_page or node is difference
printers[node] = (make_printer, target) if single_page else (file_printer, target, "%s.html" % anchor)
stored = node
for child in node.details:
ancestors[child] = stored
- maybe_print(stored)
+ conts = continuations.setdefault(stored, [])
+ if node_continuation:
+ conts.append(node_continuation)
+
+ self.maybe_print(stored, printers, outputs, continuations)
if outputs:
import pprint
diff --git a/diffoscope/presenters/html/templates.py b/diffoscope/presenters/html/templates.py
index 595bc58..29ad061 100644
--- a/diffoscope/presenters/html/templates.py
+++ b/diffoscope/presenters/html/templates.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with diffoscope. If not, see <https://www.gnu.org/licenses/>.
-HEADER = """<!DOCTYPE html>
+HEADER = u"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -147,12 +147,12 @@ HEADER = """<!DOCTYPE html>
<body class="diffoscope">
"""
-FOOTER = """<div class="footer">Generated by <a href="https://diffoscope.org" rel="noopener noreferrer" target="_blank">diffoscope</a> %(version)s</div>
+FOOTER = u"""<div class="footer">Generated by <a href="https://diffoscope.org" rel="noopener noreferrer" target="_blank">diffoscope</a> %(version)s</div>
</body>
</html>
"""
-SCRIPTS = """<script src="%(jquery_url)s"></script>
+SCRIPTS = u"""<script src="%(jquery_url)s"></script>
<script type="text/javascript">
$(function() {
// activate "loading" controls
@@ -209,7 +209,10 @@ $(function() {
</script>
"""
-DIFFNODE_LAZY_LOAD = """<div class="ondemand-details">... <a href="%s.html">load details</a> ...</div>
+DIFFNODE_LAZY_LOAD = u"""<div class="ondemand-details">... <a href="%s.html">load details</a> ...</div>
+"""
+
+DIFFNODE_LIMIT = u"""<div class="error">Max report size reached</div>
"""
UD_TABLE_HEADER = u"""<table class="diff">
@@ -220,5 +223,8 @@ UD_TABLE_HEADER = u"""<table class="diff">
UD_TABLE_FOOTER = u"""<tr class="ondemand"><td colspan="4">
... <a href="%(filename)s">%(text)s</a> ...
</td></tr>
-</table>
"""
+
+UD_TABLE_LIMIT_FOOTER = u"""<tr class="error"><td colspan="4">
+Max %(limit_type)s reached; %(bytes_left)s/%(bytes_total)s bytes (%(percent).2f%%) of diff not shown.
+</td></tr>"""
diff --git a/diffoscope/presenters/utils.py b/diffoscope/presenters/utils.py
index 53a1d3a..a43c6f3 100644
--- a/diffoscope/presenters/utils.py
+++ b/diffoscope/presenters/utils.py
@@ -259,6 +259,10 @@ class PartialString(object):
real_mapping, new_holes = self._pformat(mapping)
return self.__class__(self._format(*real_mapping), *new_holes)
+ def pformatl(self, *args):
+ """Partially apply a list, implicitly mapped from self.holes."""
+ return self.pformat(dict(zip(self.holes, args)))
+
def format(self, mapping={}):
"""Fully apply a mapping, returning a string."""
real_mapping, new_holes = self._pformat(mapping)
@@ -277,6 +281,19 @@ class PartialString(object):
return cls("{0}", obj)
@classmethod
+ def numl(cls, fmtstr="", nargs=0, *holes):
+ """Create a partial string with some implicit numeric holes.
+
+ Useful in conjuction with PartialString.pformatl.
+
+ >>> PartialString.numl("{0}{1}{2}{3}", 3, "last object")
+ PartialString('{0}{1}{2}{3}', 0, 1, 2, 'last object')
+ >>> _.pformatl(40, 41, 42, "final")
+ PartialString('404142final',)
+ """
+ return cls(fmtstr, *range(nargs), *holes)
+
+ @classmethod
def cont(cls):
r"""Create a new empty partial string with a continuation token.
@@ -296,7 +313,10 @@ class PartialString(object):
27
"""
def cont(t, fmtstr, *holes):
- return t.pformat({cont: cls(fmtstr, *(holes + (cont,)))})
+ if isinstance(fmtstr, cls):
+ return t.pformat({cont: fmtstr})
+ else:
+ return t.pformat({cont: cls(fmtstr, *(holes + (cont,)))})
return cls("{0}", cont), cont
def frame(self, header, footer):
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/diffoscope.git
More information about the diffoscope
mailing list