# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Methods for converting model objects to human-readable formats."""
import abc
import data_quality
import io
import collections
import csv
import datetime
import itertools
import math
import time
import models
def _PrettySize(size):
# Arbitrarily chosen cut-off.
if abs(size) < 2000:
return '%d bytes' % size
# Always show 3 digits.
size /= 1024.0
if abs(size) < 10:
return '%.2fkb' % size
if abs(size) < 100:
return '%.1fkb' % size
if abs(size) < 1024:
return '%dkb' % size
size /= 1024.0
if abs(size) < 10:
return '%.2fmb' % size
# We shouldn't be seeing sizes > 100mb.
return '%.1fmb' % size
def _FormatPss(pss, force_sign=False):
# Shows a decimal for small numbers to make it clear that a shared symbol has
# a non-zero pss.
if abs(pss) > 10:
return str(int(pss))
near_int = abs(pss) % 1 < 0.05
if near_int and abs(pss) < 1 and pss:
return '~0'
if force_sign:
return ('%+.0f' if near_int else '%+.1f') % pss
return ('%.0f' if near_int else '%.1f') % pss
def _Divide(a, b):
return float(a) / b if b else 0
def _GetSectionSizeInfo(unsummed_sections, summed_sections, section_sizes):
sizes = [v for k, v in section_sizes.items() if k in summed_sections]
total_bytes = sum(sizes)
max_bytes = max(sizes)
maybe_significant_sections = unsummed_sections | summed_sections
def is_significant_section(name, size):
# Show all sections containing symbols, plus relocations.
# As a catch-all, also include any section that comprises > 4% of the
# largest section. Use largest section rather than total so that it still
# works out when showing a diff containing +100, -100 (total=0).
return (name in maybe_significant_sections
or name in ['.rela.dyn', '.rel.dyn']
or abs(_Divide(size, max_bytes)) > .04)
section_names = sorted(
k for k, v in section_sizes.items() if is_significant_section(k, v))
return (total_bytes, section_names)
class Histogram:
BUCKET_NAMES_FOR_SMALL_VALUES = {-1: '(-1,0)', 0: '{0}', 1: '(0,1)'}
def __init__(self):
self.data = collections.defaultdict(int)
# Input: (-8,-4], (-4,-2], (-2,-1], (-1,0), {0}, (0,1), [1,2), [2,4), [4,8).
# Output: -4, -3, -2, -1, 0, 1, 2, 3, 4.
@staticmethod
def _Bucket(v):
absv = abs(v)
if absv < 1:
return 0 if v == 0 else (-1 if v < 0 else 1)
mag = int(math.log(absv, 2.0)) + 2
return mag if v > 0 else -mag
@staticmethod
def _BucketName(k):
if abs(k) <= 1:
return Histogram.BUCKET_NAMES_FOR_SMALL_VALUES[k]
if k < 0:
return '(-{},-{}]'.format(1 << (-k - 1), 1 << (-k - 2))
return '[{},{})'.format(1 << (k - 2), 1 << (k - 1))
def Add(self, v):
self.data[self._Bucket(v)] += 1
def Generate(self):
keys = sorted(self.data.keys())
bucket_names = [self._BucketName(k) for k in keys]
bucket_values = [str(self.data[k]) for k in keys]
num_items = len(keys)
num_cols = 6
num_rows = (num_items + num_cols - 1) // num_cols # Divide and round up.
# Needed for range() to not throw due to step by 0.
if num_rows == 0:
return
# Spaces needed by items in each column, to align on ':'.
name_col_widths = []
value_col_widths = []
for i in range(0, num_items, num_rows):
name_col_widths.append(max(len(s) for s in bucket_names[i:][:num_rows]))
value_col_widths.append(max(len(s) for s in bucket_values[i:][:num_rows]))
yield 'Histogram of symbols based on PSS:'
for r in range(num_rows):
row = list(
zip(bucket_names[r::num_rows], name_col_widths,
bucket_values[r::num_rows], value_col_widths))
line = ' ' + ' '.join('{:>{}}: {:<{}}'.format(*t) for t in row)
yield line.rstrip()
class Describer:
def __init__(self):
pass
@abc.abstractmethod
def _DescribeDeltaSizeInfo(self, diff):
pass
@abc.abstractmethod
def _DescribeSizeInfo(self, size_info):
pass
@abc.abstractmethod
def _DescribeDeltaSymbolGroup(self, delta_group):
pass
@abc.abstractmethod
def _DescribeSymbolGroup(self, group):
pass
@abc.abstractmethod
def _DescribeSymbol(self, sym, single_line=False):
pass
def _DescribeIterable(self, obj):
for i, x in enumerate(obj):
yield '{}: {!r}'.format(i, x)
def GenerateLines(self, obj):
if isinstance(obj, models.DeltaSizeInfo):
return self._DescribeDeltaSizeInfo(obj)
if isinstance(obj, models.SizeInfo):
return self._DescribeSizeInfo(obj)
if isinstance(obj, models.DeltaSymbolGroup):
return self._DescribeDeltaSymbolGroup(obj)
if isinstance(obj, models.SymbolGroup):
return self._DescribeSymbolGroup(obj)
if isinstance(obj, (models.Symbol, models.DeltaSymbol)):
return self._DescribeSymbol(obj)
if hasattr(obj, '__iter__'):
return self._DescribeIterable(obj)
return iter((repr(obj),))
class DescriberText(Describer):
def __init__(self, verbose=False, recursive=False, summarize=True):
super().__init__()
self.verbose = verbose
self.recursive = recursive
self.summarize = summarize
def _DescribeSectionSizes(self,
unsummed_sections,
summed_sections,
section_sizes,
indent=''):
total_bytes, section_names = _GetSectionSizeInfo(unsummed_sections,
summed_sections,
section_sizes)
yield ''
yield '{}Section Sizes (Total={} ({} bytes)):'.format(
indent, _PrettySize(total_bytes), total_bytes)
for name in section_names:
size = section_sizes[name]
if name in unsummed_sections:
yield '{} {}: {} ({} bytes) (not included in totals)'.format(
indent, name, _PrettySize(size), size)
else:
notes = ''
if name not in summed_sections:
notes = ' (counted in .other)'
percent = _Divide(size, total_bytes)
yield '{} {}: {} ({} bytes) ({:.1%}){}'.format(
indent, name, _PrettySize(size), size, percent, notes)
if self.verbose:
yield ''
yield '{}Other section sizes:'.format(indent)
section_names = sorted(
k for k in section_sizes.keys() if k not in section_names)
for name in section_names:
notes = ''
if name in unsummed_sections:
notes = ' (not included in totals)'
elif name not in summed_sections:
notes = ' (counted in .other)'
yield '{} {}: {} ({} bytes){}'.format(
indent, name, _PrettySize(section_sizes[name]), section_sizes[name],
notes)
def _DescribeSymbol(self, sym, single_line=False):
container_str = sym.container_short_name
if container_str:
container_str = '<{}>'.format(container_str)
address = 'Group' if sym.IsGroup() else hex(sym.address)
last_field = ''
if sym.IsGroup():
last_field = 'count=%d' % len(sym)
else:
syms = [sym.before_symbol, sym.after_symbol] if sym.IsDelta() else [sym]
num_aliases = [s.num_aliases for s in syms if not s is None]
if num_aliases[0] != num_aliases[-1]: # If 2 distinct values.
last_field = 'num_aliases=%d->%d' % tuple(num_aliases)
elif num_aliases[0] > 1 or self.verbose:
last_field = 'num_aliases=%d' % num_aliases[0]
pss_field = _FormatPss(sym.pss, sym.IsDelta())
if sym.IsDelta():
b = sum(s.before_symbol.pss_without_padding if s.before_symbol else 0
for s in sym.IterLeafSymbols())
a = sum(s.after_symbol.pss_without_padding if s.after_symbol else 0
for s in sym.IterLeafSymbols())
pss_field = '{} ({}->{})'.format(pss_field, _FormatPss(b), _FormatPss(a))
elif sym.num_aliases > 1:
pss_field = '{} (size={})'.format(pss_field, sym.size)
if self.verbose:
if last_field:
last_field = ' ' + last_field
if sym.IsDelta():
yield '{}{}@{:<9s} {}{}'.format(container_str, sym.section, address,
pss_field, last_field)
else:
l = '{}{}@{:<9s} pss={} padding={}{}'.format(container_str,
sym.section, address,
pss_field, sym.padding,
last_field)
yield l
yield ' source_path={} \tobject_path={}'.format(
sym.source_path, sym.object_path)
if sym.name:
yield ' flags={} name={}'.format(sym.FlagsString(), sym.name)
if sym.full_name is not sym.name:
yield ' full_name={}'.format(sym.full_name)
elif sym.full_name:
yield ' flags={} full_name={}'.format(
sym.FlagsString(), sym.full_name)
else:
if last_field:
last_field = ' ({})'.format(last_field)
if sym.IsDelta():
pss_field = '{:<18}'.format(pss_field)
else:
pss_field = '{:<14}'.format(pss_field)
if single_line:
yield '{}{}@{:<9s} {} {}{}'.format(container_str, sym.section,
address, pss_field, sym.name,
last_field)
else:
path = sym.source_path or sym.object_path
if path and sym.generated_source:
path = '$root_gen_dir/' + path
path = path or '{no path}'
yield '{}{}@{:<9s} {} {}'.format(container_str, sym.section, address,
pss_field, path)
if sym.name:
yield ' {}{}'.format(sym.name, last_field)
def _DescribeSymbolGroupChildren(self, group, indent=0):
running_total = 0
running_percent = 0
is_delta = group.IsDelta()
all_groups = all(s.IsGroup() for s in group)
indent_prefix = '> ' * indent
diff_prefix = ''
total = group.pss
# is_default_sorted ==> sorted by abs(PSS) from largest to smallest.
if group.is_default_sorted:
# Skip long tail of small symbols (useful for diffs where aliases change).
# Long tail is defined as:
# * Accounts for < .5% of PSS
# * Symbols are smaller than 1.0 byte (by PSS)
# * Always show at least 50 symbols.
min_remaining_pss_to_show = max(1024.0, total / 1000.0 * 5)
min_symbol_pss_to_show = 1.0
min_symbols_to_show = 50
for index, s in enumerate(group):
if group.is_default_sorted and not self.verbose:
remaining_pss = total - running_total
if (index >= min_symbols_to_show and
abs(remaining_pss) < min_remaining_pss_to_show and
abs(s.pss) < min_symbol_pss_to_show):
remaining_count = len(group) - index
yield '{}Skipping {} tiny symbols comprising {} bytes.'.format(
indent_prefix, remaining_count, _FormatPss(remaining_pss))
break
if group.IsBss() or not s.IsBss():
running_total += s.pss
running_percent = _Divide(running_total, total)
for l in self._DescribeSymbol(s, single_line=all_groups):
if l[:4].isspace():
indent_size = 8 + len(indent_prefix) + len(diff_prefix)
yield '{} {}'.format(' ' * indent_size, l)
else:
if is_delta:
diff_prefix = models.DIFF_PREFIX_BY_STATUS[s.diff_status]
yield '{}{}{:<4} {:>8} {:7} {}'.format(
indent_prefix, diff_prefix, str(index) + ')',
_FormatPss(running_total), '({:.1%})'.format(running_percent), l)
if self.recursive and s.IsGroup():
for l in self._DescribeSymbolGroupChildren(s, indent=indent + 1):
yield l
@staticmethod
def _RelevantSections(section_names):
relevant_sections = [
s for s in models.SECTION_TO_SECTION_NAME.values() if s in section_names
]
if models.SECTION_MULTIPLE in relevant_sections:
relevant_sections.remove(models.SECTION_MULTIPLE)
return relevant_sections
def _DescribeSymbolGroup(self, group):
if self.summarize:
total_size = group.pss
pss_by_section = collections.defaultdict(float)
counts_by_section = collections.defaultdict(int)
for s in group.IterLeafSymbols():
pss_by_section[s.section_name] += s.pss
if not s.IsDelta() or s.diff_status is not models.DIFF_STATUS_UNCHANGED:
counts_by_section[s.section_name] += 1
# Apply this filter after calcualating size since an alias being removed
# causes some symbols to be UNCHANGED, yet have pss != 0.
if group.IsDelta():
group = group.WhereDiffStatusIs(models.DIFF_STATUS_UNCHANGED).Inverted()
if self.summarize:
histogram = Histogram()
for s in group:
histogram.Add(s.pss)
unique_paths = set()
for s in group.IterLeafSymbols():
# Ignore paths like foo/{shared}/2
if '{' not in s.object_path:
unique_paths.add(s.object_path)
if group.IsDelta():
before_unique, after_unique = group.CountUniqueSymbols()
unique_part = '{:,} -> {:,} unique'.format(before_unique, after_unique)
else:
unique_part = '{:,} unique'.format(group.CountUniqueSymbols())
relevant_sections = self._RelevantSections(pss_by_section)
size_summary = 'Sizes: ' + ' '.join(
'{}={:<10}'.format(k, _PrettySize(int(pss_by_section[k])))
for k in relevant_sections)
size_summary += ' total={:<10}'.format(_PrettySize(int(total_size)))
counts_summary = 'Counts: ' + ' '.join(
'{}={}'.format(k, counts_by_section[k]) for k in relevant_sections)
section_legend = ', '.join(
'{}={}'.format(models.SECTION_NAME_TO_SECTION[k], k)
for k in relevant_sections if k in models.SECTION_NAME_TO_SECTION)
summary_desc = itertools.chain(
['Showing {:,} symbols ({}) with total pss: {} bytes'.format(
len(group), unique_part, int(total_size))],
histogram.Generate(),
[size_summary.rstrip()],
[counts_summary],
['Number of unique paths: {}'.format(len(unique_paths))],
[''],
['Section Legend: {}'.format(section_legend)],
)
else:
summary_desc = ()
title_parts = ['Index', 'Running Total']
if group.container_name == '':
title_parts.append('Section@Address')
else:
title_parts.append('<Container>Section@Address')
if self.verbose:
title_parts.append('...')
else:
if group.IsDelta():
title_parts.append(u'\u0394 PSS (\u0394 size_without_padding)')
else:
title_parts.append('PSS')
title_parts.append('Path')
titles = ' | '.join(title_parts)
header_desc = (titles, '-' * 60)
children_desc = self._DescribeSymbolGroupChildren(group)
return itertools.chain(summary_desc, header_desc, children_desc)
def _DescribeDiffObjectPaths(self, delta_group):
paths_by_status = [set(), set(), set(), set()]
for s in delta_group.IterLeafSymbols():
path = s.source_path or s.object_path
# Ignore paths like foo/{shared}/2
if '{' not in path:
paths_by_status[s.diff_status].add(path)
# Initial paths sets are those where *any* symbol is
# unchanged/changed/added/removed.
unchanged, changed, added, removed = paths_by_status
# Consider a path with both adds & removes as "changed".
changed.update(added.intersection(removed))
# Consider a path added / removed only when all symbols are new/removed.
added.difference_update(unchanged)
added.difference_update(changed)
added.difference_update(removed)
removed.difference_update(unchanged)
removed.difference_update(changed)
removed.difference_update(added)
yield '{} paths added, {} removed, {} changed'.format(
len(added), len(removed), len(changed))
if self.verbose and added:
yield 'Added files:'
for p in sorted(added):
yield ' ' + p
if self.verbose and removed:
yield 'Removed files:'
for p in sorted(removed):
yield ' ' + p
if self.verbose and changed:
yield 'Changed files:'
for p in sorted(changed):
yield ' ' + p
def _DescribeDeltaSymbolGroup(self, delta_group):
if self.summarize:
num_inc = 0
num_dec = 0
counts_by_section = collections.defaultdict(int)
for sym in delta_group.IterLeafSymbols():
if sym.pss > 0:
num_inc += 1
elif sym.pss < 0:
num_dec += 1
status = sym.diff_status
if status == models.DIFF_STATUS_ADDED:
counts_by_section[sym.section_name] += 1
elif status == models.DIFF_STATUS_REMOVED:
counts_by_section[sym.section_name] -= 1
relevant_sections = self._RelevantSections(counts_by_section)
counts = delta_group.CountsByDiffStatus()
diff_status_msg = ('{} symbols added (+), {} changed (~), '
'{} removed (-), {} unchanged (not shown)').format(
counts[models.DIFF_STATUS_ADDED],
counts[models.DIFF_STATUS_CHANGED],
counts[models.DIFF_STATUS_REMOVED],
counts[models.DIFF_STATUS_UNCHANGED])
counts_by_section_msg = 'Added/Removed by section: ' + ' '.join(
'{}: {:+}'.format(k, counts_by_section[k]) for k in relevant_sections)
num_unique_before_symbols, num_unique_after_symbols = (
delta_group.CountUniqueSymbols())
diff_summary_desc = [
diff_status_msg,
counts_by_section_msg,
'Of changed symbols, {} grew, {} shrank'.format(num_inc, num_dec),
'Number of unique symbols {} -> {} ({:+})'.format(
num_unique_before_symbols, num_unique_after_symbols,
num_unique_after_symbols - num_unique_before_symbols),
]
path_delta_desc = itertools.chain(
self._DescribeDiffObjectPaths(delta_group),
('',))
else:
diff_summary_desc = ()
path_delta_desc = ()
group_desc = self._DescribeSymbolGroup(delta_group)
return itertools.chain(diff_summary_desc, path_delta_desc, group_desc)
def _DescribeDeltaDict(self, data_name, before_dict, after_dict, indent=''):
common_items = {
k: v
for k, v in before_dict.items() if after_dict.get(k) == v
}
before_items = {
k: v
for k, v in before_dict.items() if k not in common_items
}
after_items = {k: v for k, v in after_dict.items() if k not in common_items}
return itertools.chain(
(indent + 'Common %s:' % data_name, ),
(indent + ' %s' % line for line in DescribeDict(common_items)),
(indent + 'Old %s:' % data_name, ),
(indent + ' %s' % line for line in DescribeDict(before_items)),
(indent + 'New %s:' % data_name, ),
(indent + ' %s' % line for line in DescribeDict(after_items)))
def _DescribeDeltaSizeInfo(self, diff):
desc_list = []
# Describe |build_config| and each container. If there is only one container
# then support legacy output by reporting |build_config| as part of the
# first container's metadata.
if len(diff.containers) > 1:
desc_list.append(
self._DescribeDeltaDict('Build config', diff.before.build_config,
diff.after.build_config))
for c in diff.containers:
desc_list.append(('', ))
desc_list.append(('Container<%s>: %s' % (c.short_name, c.name), ))
desc_list.append(
self._DescribeDeltaDict('Metadata',
c.before.metadata,
c.after.metadata,
indent=' '))
unsummed_sections, summed_sections = c.ClassifySections()
desc_list.append(
self._DescribeSectionSizes(unsummed_sections,
summed_sections,
c.section_sizes,
indent=' '))
else: # Legacy output for single Container case.
desc_list.append(
self._DescribeDeltaDict('Metadata', diff.before.metadata_legacy,
diff.after.metadata_legacy))
c = diff.containers[0]
unsummed_sections, summed_sections = c.ClassifySections()
desc_list.append(
self._DescribeSectionSizes(unsummed_sections, summed_sections,
c.section_sizes))
desc_list.append(('', ))
desc_list.append(self.GenerateLines(diff.symbols))
return itertools.chain.from_iterable(desc_list)
def _DescribeSizeInfo(self, size_info):
desc_list = []
# Describe |build_config| and each container. If there is only one container
# then support legacy output by reporting |build_config| as part of the
# first container's metadata.
if len(size_info.containers) > 1:
desc_list.append(('Build Configs:', ))
desc_list.append(' %s' % line
for line in DescribeDict(size_info.build_config))
containers = size_info.containers
else:
containers = [
models.Container(
name='',
metadata=size_info.metadata_legacy,
section_sizes=size_info.containers[0].section_sizes,
metrics_by_file=size_info.containers[0].metrics_by_file)
]
for c in containers:
if c.name:
desc_list.append(('', ))
desc_list.append(('Container<%s>: %s' % (c.short_name, c.name), ))
desc_list.append(('Metadata:', ))
desc_list.append(' %s' % line for line in DescribeDict(c.metadata))
unsummed_sections, summed_sections = c.ClassifySections()
desc_list.append(
self._DescribeSectionSizes(unsummed_sections, summed_sections,
c.section_sizes))
if self.verbose:
desc_list.append(('', ))
desc_list.append(data_quality.DescribeSizeInfoCoverage(size_info))
desc_list.append(('', ))
desc_list.append(self.GenerateLines(size_info.symbols))
return itertools.chain.from_iterable(desc_list)
class DescriberCsv(Describer):
def __init__(self, verbose=False):
super().__init__()
self.verbose = verbose
self.stringio = io.StringIO()
self.csv_writer = csv.writer(self.stringio)
def _RenderCsv(self, data):
self.stringio.truncate(0)
self.stringio.seek(0)
self.csv_writer.writerow(data)
return self.stringio.getvalue().rstrip()
def _DescribeSectionSizes(self, unsummed_sections, summed_section,
section_sizes):
_, significant_section_names = _GetSectionSizeInfo(unsummed_sections,
summed_section,
section_sizes)
if self.verbose:
significant_set = set(significant_section_names)
section_names = sorted(section_sizes.keys())
yield self._RenderCsv(['Name', 'Size', 'IsSignificant'])
for name in section_names:
size = section_sizes[name]
yield self._RenderCsv([name, size, int(name in significant_set)])
else:
yield self._RenderCsv(['Name', 'Size'])
for name in significant_section_names:
size = section_sizes[name]
yield self._RenderCsv([name, size])
def _DescribeDeltaSizeInfo(self, diff):
desc_list = []
for c in diff.containers:
unsummed_sections, summed_sections = c.ClassifySections()
desc_list.append(
self._DescribeSectionSizes(unsummed_sections, summed_sections,
c.section_sizes))
desc_list.append(('', ))
desc_list.append(self.GenerateLines(diff.symbols))
return itertools.chain.from_iterable(desc_list)
def _DescribeSizeInfo(self, size_info):
desc_list = []
for c in size_info.containers:
unsummed_sections, summed_sections = c.ClassifySections()
desc_list.append(
self._DescribeSectionSizes(unsummed_sections, summed_sections,
c.section_sizes))
desc_list.append(('', ))
desc_list.append(self.GenerateLines(size_info.symbols))
return itertools.chain.from_iterable(desc_list)
def _DescribeDeltaSymbolGroup(self, delta_group):
yield self._RenderSymbolHeader(True);
# Apply filter to remove UNCHANGED groups.
delta_group = delta_group.WhereDiffStatusIs(
models.DIFF_STATUS_UNCHANGED).Inverted()
for sym in delta_group:
yield self._RenderSymbolData(sym)
def _DescribeSymbolGroup(self, group):
yield self._RenderSymbolHeader(False);
for sym in group:
yield self._RenderSymbolData(sym)
def _DescribeSymbol(self, sym, single_line=False):
yield self._RenderSymbolHeader(sym.IsDelta());
yield self._RenderSymbolData(sym)
def _RenderSymbolHeader(self, isDelta):
fields = []
fields.append('GroupCount')
fields.append('Address')
fields.append('SizeWithoutPadding')
fields.append('Padding')
if isDelta:
fields += ['BeforeNumAliases', 'AfterNumAliases']
else:
fields.append('NumAliases')
fields.append('PSS')
fields.append('Section')
if self.verbose:
fields.append('Flags')
fields.append('SourcePath')
fields.append('ObjectPath')
fields.append('Name')
if self.verbose:
fields.append('FullName')
return self._RenderCsv(fields)
def _RenderSymbolData(self, sym):
data = []
data.append(len(sym) if sym.IsGroup() else None)
data.append(None if sym.IsGroup() else hex(sym.address))
data.append(sym.size_without_padding)
data.append(sym.padding)
if sym.IsDelta():
b, a = (None, None) if sym.IsGroup() else (sym.before_symbol,
sym.after_symbol)
data.append(b.num_aliases if b else None)
data.append(a.num_aliases if a else None)
else:
data.append(sym.num_aliases)
data.append(round(sym.pss, 3))
data.append(sym.section)
if self.verbose:
data.append(sym.FlagsString())
data.append(sym.source_path);
data.append(sym.object_path);
data.append(sym.name)
if self.verbose:
data.append(sym.full_name)
return self._RenderCsv(data)
def _UtcToLocal(utc):
epoch = time.mktime(utc.timetuple())
offset = (datetime.datetime.fromtimestamp(epoch) -
datetime.datetime.utcfromtimestamp(epoch))
return utc + offset
def DescribeDict(input_dict):
display_dict = {}
for k, v in input_dict.items():
if k == models.METADATA_ELF_MTIME:
timestamp_obj = datetime.datetime.utcfromtimestamp(v)
display_dict[k] = (
_UtcToLocal(timestamp_obj).strftime('%Y-%m-%d %H:%M:%S'))
elif isinstance(v, str):
display_dict[k] = v
elif isinstance(v, list):
if v:
if isinstance(v[0], str):
display_dict[k] = ' '.join(str(t) for t in v)
else:
display_dict[k] = repr(v)
else:
display_dict[k] = ''
else:
display_dict[k] = repr(v)
return sorted('%s=%s' % t for t in display_dict.items())
def GenerateLines(obj, verbose=False, recursive=False, summarize=True,
format_name='text'):
"""Returns an iterable of lines (without \n) that describes |obj|."""
if format_name == 'text':
d = DescriberText(verbose=verbose, recursive=recursive, summarize=summarize)
elif format_name == 'csv':
d = DescriberCsv(verbose=verbose)
else:
raise ValueError('Unknown format_name \'{}\''.format(format_name));
return d.GenerateLines(obj)
def WriteLines(lines, func):
for l in lines:
func(l)
func('\n')