summary.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. #!/usr/bin/env python3
  2. #
  3. # Script to summarize the outputs of other scripts. Operates on CSV files.
  4. #
  5. # Example:
  6. # ./scripts/code.py lfs.o lfs_util.o -q -o lfs.code.csv
  7. # ./scripts/data.py lfs.o lfs_util.o -q -o lfs.data.csv
  8. # ./scripts/summary.py lfs.code.csv lfs.data.csv -q -o lfs.csv
  9. # ./scripts/summary.py -Y lfs.csv -f code=code_size,data=data_size
  10. #
  11. # Copyright (c) 2022, The littlefs authors.
  12. # SPDX-License-Identifier: BSD-3-Clause
  13. #
  14. import collections as co
  15. import csv
  16. import functools as ft
  17. import glob
  18. import itertools as it
  19. import math as m
  20. import os
  21. import re
  22. CSV_PATHS = ['*.csv']
  23. # supported merge operations
  24. #
  25. # this is a terrible way to express these
  26. #
  27. OPS = {
  28. 'sum': lambda xs: sum(xs[1:], start=xs[0]),
  29. 'prod': lambda xs: m.prod(xs[1:], start=xs[0]),
  30. 'min': min,
  31. 'max': max,
  32. 'mean': lambda xs: Float(sum(float(x) for x in xs) / len(xs)),
  33. 'stddev': lambda xs: (
  34. lambda mean: Float(
  35. m.sqrt(sum((float(x) - mean)**2 for x in xs) / len(xs)))
  36. )(sum(float(x) for x in xs) / len(xs)),
  37. 'gmean': lambda xs: Float(m.prod(float(x) for x in xs)**(1/len(xs))),
  38. 'gstddev': lambda xs: (
  39. lambda gmean: Float(
  40. m.exp(m.sqrt(sum(m.log(float(x)/gmean)**2 for x in xs) / len(xs)))
  41. if gmean else m.inf)
  42. )(m.prod(float(x) for x in xs)**(1/len(xs))),
  43. }
  44. # integer fields
  45. class Int(co.namedtuple('Int', 'x')):
  46. __slots__ = ()
  47. def __new__(cls, x=0):
  48. if isinstance(x, Int):
  49. return x
  50. if isinstance(x, str):
  51. try:
  52. x = int(x, 0)
  53. except ValueError:
  54. # also accept +-∞ and +-inf
  55. if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
  56. x = m.inf
  57. elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
  58. x = -m.inf
  59. else:
  60. raise
  61. assert isinstance(x, int) or m.isinf(x), x
  62. return super().__new__(cls, x)
  63. def __str__(self):
  64. if self.x == m.inf:
  65. return '∞'
  66. elif self.x == -m.inf:
  67. return '-∞'
  68. else:
  69. return str(self.x)
  70. def __int__(self):
  71. assert not m.isinf(self.x)
  72. return self.x
  73. def __float__(self):
  74. return float(self.x)
  75. none = '%7s' % '-'
  76. def table(self):
  77. return '%7s' % (self,)
  78. diff_none = '%7s' % '-'
  79. diff_table = table
  80. def diff_diff(self, other):
  81. new = self.x if self else 0
  82. old = other.x if other else 0
  83. diff = new - old
  84. if diff == +m.inf:
  85. return '%7s' % '+∞'
  86. elif diff == -m.inf:
  87. return '%7s' % '-∞'
  88. else:
  89. return '%+7d' % diff
  90. def ratio(self, other):
  91. new = self.x if self else 0
  92. old = other.x if other else 0
  93. if m.isinf(new) and m.isinf(old):
  94. return 0.0
  95. elif m.isinf(new):
  96. return +m.inf
  97. elif m.isinf(old):
  98. return -m.inf
  99. elif not old and not new:
  100. return 0.0
  101. elif not old:
  102. return 1.0
  103. else:
  104. return (new-old) / old
  105. def __add__(self, other):
  106. return self.__class__(self.x + other.x)
  107. def __sub__(self, other):
  108. return self.__class__(self.x - other.x)
  109. def __mul__(self, other):
  110. return self.__class__(self.x * other.x)
  111. # float fields
  112. class Float(co.namedtuple('Float', 'x')):
  113. __slots__ = ()
  114. def __new__(cls, x=0.0):
  115. if isinstance(x, Float):
  116. return x
  117. if isinstance(x, str):
  118. try:
  119. x = float(x)
  120. except ValueError:
  121. # also accept +-∞ and +-inf
  122. if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
  123. x = m.inf
  124. elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
  125. x = -m.inf
  126. else:
  127. raise
  128. assert isinstance(x, float), x
  129. return super().__new__(cls, x)
  130. def __str__(self):
  131. if self.x == m.inf:
  132. return '∞'
  133. elif self.x == -m.inf:
  134. return '-∞'
  135. else:
  136. return '%.1f' % self.x
  137. def __float__(self):
  138. return float(self.x)
  139. none = Int.none
  140. table = Int.table
  141. diff_none = Int.diff_none
  142. diff_table = Int.diff_table
  143. diff_diff = Int.diff_diff
  144. ratio = Int.ratio
  145. __add__ = Int.__add__
  146. __sub__ = Int.__sub__
  147. __mul__ = Int.__mul__
  148. # fractional fields, a/b
  149. class Frac(co.namedtuple('Frac', 'a,b')):
  150. __slots__ = ()
  151. def __new__(cls, a=0, b=None):
  152. if isinstance(a, Frac) and b is None:
  153. return a
  154. if isinstance(a, str) and b is None:
  155. a, b = a.split('/', 1)
  156. if b is None:
  157. b = a
  158. return super().__new__(cls, Int(a), Int(b))
  159. def __str__(self):
  160. return '%s/%s' % (self.a, self.b)
  161. def __float__(self):
  162. return float(self.a)
  163. none = '%11s %7s' % ('-', '-')
  164. def table(self):
  165. t = self.a.x/self.b.x if self.b.x else 1.0
  166. return '%11s %7s' % (
  167. self,
  168. '∞%' if t == +m.inf
  169. else '-∞%' if t == -m.inf
  170. else '%.1f%%' % (100*t))
  171. diff_none = '%11s' % '-'
  172. def diff_table(self):
  173. return '%11s' % (self,)
  174. def diff_diff(self, other):
  175. new_a, new_b = self if self else (Int(0), Int(0))
  176. old_a, old_b = other if other else (Int(0), Int(0))
  177. return '%11s' % ('%s/%s' % (
  178. new_a.diff_diff(old_a).strip(),
  179. new_b.diff_diff(old_b).strip()))
  180. def ratio(self, other):
  181. new_a, new_b = self if self else (Int(0), Int(0))
  182. old_a, old_b = other if other else (Int(0), Int(0))
  183. new = new_a.x/new_b.x if new_b.x else 1.0
  184. old = old_a.x/old_b.x if old_b.x else 1.0
  185. return new - old
  186. def __add__(self, other):
  187. return self.__class__(self.a + other.a, self.b + other.b)
  188. def __sub__(self, other):
  189. return self.__class__(self.a - other.a, self.b - other.b)
  190. def __mul__(self, other):
  191. return self.__class__(self.a * other.a, self.b + other.b)
  192. def __lt__(self, other):
  193. self_t = self.a.x/self.b.x if self.b.x else 1.0
  194. other_t = other.a.x/other.b.x if other.b.x else 1.0
  195. return (self_t, self.a.x) < (other_t, other.a.x)
  196. def __gt__(self, other):
  197. return self.__class__.__lt__(other, self)
  198. def __le__(self, other):
  199. return not self.__gt__(other)
  200. def __ge__(self, other):
  201. return not self.__lt__(other)
  202. # available types
  203. TYPES = co.OrderedDict([
  204. ('int', Int),
  205. ('float', Float),
  206. ('frac', Frac)
  207. ])
  208. def infer(results, *,
  209. by=None,
  210. fields=None,
  211. types={},
  212. ops={},
  213. renames=[],
  214. **_):
  215. # if fields not specified, try to guess from data
  216. if fields is None:
  217. fields = co.OrderedDict()
  218. for r in results:
  219. for k, v in r.items():
  220. if (by is None or k not in by) and v.strip():
  221. types_ = []
  222. for t in fields.get(k, TYPES.values()):
  223. try:
  224. t(v)
  225. types_.append(t)
  226. except ValueError:
  227. pass
  228. fields[k] = types_
  229. fields = list(k for k, v in fields.items() if v)
  230. # deduplicate fields
  231. fields = list(co.OrderedDict.fromkeys(fields).keys())
  232. # if by not specified, guess it's anything not in fields and not a
  233. # source of a rename
  234. if by is None:
  235. by = co.OrderedDict()
  236. for r in results:
  237. # also ignore None keys, these are introduced by csv.DictReader
  238. # when header + row mismatch
  239. by.update((k, True) for k in r.keys()
  240. if k is not None
  241. and k not in fields
  242. and not any(k == old_k for _, old_k in renames))
  243. by = list(by.keys())
  244. # deduplicate fields
  245. by = list(co.OrderedDict.fromkeys(by).keys())
  246. # find best type for all fields
  247. types_ = {}
  248. for k in fields:
  249. if k in types:
  250. types_[k] = types[k]
  251. else:
  252. for t in TYPES.values():
  253. for r in results:
  254. if k in r and r[k].strip():
  255. try:
  256. t(r[k])
  257. except ValueError:
  258. break
  259. else:
  260. types_[k] = t
  261. break
  262. else:
  263. print("error: no type matches field %r?" % k)
  264. sys.exit(-1)
  265. types = types_
  266. # does folding change the type?
  267. types_ = {}
  268. for k, t in types.items():
  269. types_[k] = ops.get(k, OPS['sum'])([t()]).__class__
  270. # create result class
  271. def __new__(cls, **r):
  272. return cls.__mro__[1].__new__(cls,
  273. **{k: r.get(k) for k in by},
  274. **{k: r[k] if k in r and isinstance(r[k], list)
  275. else [types[k](r[k])] if k in r
  276. else []
  277. for k in fields})
  278. def __add__(self, other):
  279. return self.__class__(
  280. **{k: getattr(self, k) for k in by},
  281. **{k: object.__getattribute__(self, k)
  282. + object.__getattribute__(other, k)
  283. for k in fields})
  284. def __getattribute__(self, k):
  285. if k in fields:
  286. if object.__getattribute__(self, k):
  287. return ops.get(k, OPS['sum'])(object.__getattribute__(self, k))
  288. else:
  289. return None
  290. return object.__getattribute__(self, k)
  291. return type('Result', (co.namedtuple('Result', by + fields),), {
  292. '__slots__': (),
  293. '__new__': __new__,
  294. '__add__': __add__,
  295. '__getattribute__': __getattribute__,
  296. '_by': by,
  297. '_fields': fields,
  298. '_types': types_,
  299. })
  300. def fold(Result, results, *,
  301. by=None,
  302. defines=None,
  303. **_):
  304. if by is None:
  305. by = Result._by
  306. for k in it.chain(by or [], (k for k, _ in defines or [])):
  307. if k not in Result._by and k not in Result._fields:
  308. print("error: could not find field %r?" % k)
  309. sys.exit(-1)
  310. # filter by matching defines
  311. if defines is not None:
  312. results_ = []
  313. for r in results:
  314. if all(getattr(r, k) in vs for k, vs in defines):
  315. results_.append(r)
  316. results = results_
  317. # organize results into conflicts
  318. folding = co.OrderedDict()
  319. for r in results:
  320. name = tuple(getattr(r, k) for k in by)
  321. if name not in folding:
  322. folding[name] = []
  323. folding[name].append(r)
  324. # merge conflicts
  325. folded = []
  326. for name, rs in folding.items():
  327. folded.append(sum(rs[1:], start=rs[0]))
  328. return folded
  329. def table(Result, results, diff_results=None, *,
  330. by=None,
  331. fields=None,
  332. sort=None,
  333. summary=False,
  334. all=False,
  335. percent=False,
  336. **_):
  337. all_, all = all, __builtins__.all
  338. if by is None:
  339. by = Result._by
  340. if fields is None:
  341. fields = Result._fields
  342. types = Result._types
  343. # fold again
  344. results = fold(Result, results, by=by)
  345. if diff_results is not None:
  346. diff_results = fold(Result, diff_results, by=by)
  347. # organize by name
  348. table = {
  349. ','.join(str(getattr(r, k) or '') for k in by): r
  350. for r in results}
  351. diff_table = {
  352. ','.join(str(getattr(r, k) or '') for k in by): r
  353. for r in diff_results or []}
  354. names = list(table.keys() | diff_table.keys())
  355. # sort again, now with diff info, note that python's sort is stable
  356. names.sort()
  357. if diff_results is not None:
  358. names.sort(key=lambda n: tuple(
  359. types[k].ratio(
  360. getattr(table.get(n), k, None),
  361. getattr(diff_table.get(n), k, None))
  362. for k in fields),
  363. reverse=True)
  364. if sort:
  365. for k, reverse in reversed(sort):
  366. names.sort(key=lambda n: (getattr(table[n], k),)
  367. if getattr(table.get(n), k, None) is not None else (),
  368. reverse=reverse ^ (not k or k in Result._fields))
  369. # build up our lines
  370. lines = []
  371. # header
  372. line = []
  373. line.append('%s%s' % (
  374. ','.join(by),
  375. ' (%d added, %d removed)' % (
  376. sum(1 for n in table if n not in diff_table),
  377. sum(1 for n in diff_table if n not in table))
  378. if diff_results is not None and not percent else '')
  379. if not summary else '')
  380. if diff_results is None:
  381. for k in fields:
  382. line.append(k)
  383. elif percent:
  384. for k in fields:
  385. line.append(k)
  386. else:
  387. for k in fields:
  388. line.append('o'+k)
  389. for k in fields:
  390. line.append('n'+k)
  391. for k in fields:
  392. line.append('d'+k)
  393. line.append('')
  394. lines.append(line)
  395. # entries
  396. if not summary:
  397. for name in names:
  398. r = table.get(name)
  399. if diff_results is not None:
  400. diff_r = diff_table.get(name)
  401. ratios = [
  402. types[k].ratio(
  403. getattr(r, k, None),
  404. getattr(diff_r, k, None))
  405. for k in fields]
  406. if not any(ratios) and not all_:
  407. continue
  408. line = []
  409. line.append(name)
  410. if diff_results is None:
  411. for k in fields:
  412. line.append(getattr(r, k).table()
  413. if getattr(r, k, None) is not None
  414. else types[k].none)
  415. elif percent:
  416. for k in fields:
  417. line.append(getattr(r, k).diff_table()
  418. if getattr(r, k, None) is not None
  419. else types[k].diff_none)
  420. else:
  421. for k in fields:
  422. line.append(getattr(diff_r, k).diff_table()
  423. if getattr(diff_r, k, None) is not None
  424. else types[k].diff_none)
  425. for k in fields:
  426. line.append(getattr(r, k).diff_table()
  427. if getattr(r, k, None) is not None
  428. else types[k].diff_none)
  429. for k in fields:
  430. line.append(types[k].diff_diff(
  431. getattr(r, k, None),
  432. getattr(diff_r, k, None)))
  433. if diff_results is None:
  434. line.append('')
  435. elif percent:
  436. line.append(' (%s)' % ', '.join(
  437. '+∞%' if t == +m.inf
  438. else '-∞%' if t == -m.inf
  439. else '%+.1f%%' % (100*t)
  440. for t in ratios))
  441. else:
  442. line.append(' (%s)' % ', '.join(
  443. '+∞%' if t == +m.inf
  444. else '-∞%' if t == -m.inf
  445. else '%+.1f%%' % (100*t)
  446. for t in ratios
  447. if t)
  448. if any(ratios) else '')
  449. lines.append(line)
  450. # total
  451. r = next(iter(fold(Result, results, by=[])), None)
  452. if diff_results is not None:
  453. diff_r = next(iter(fold(Result, diff_results, by=[])), None)
  454. ratios = [
  455. types[k].ratio(
  456. getattr(r, k, None),
  457. getattr(diff_r, k, None))
  458. for k in fields]
  459. line = []
  460. line.append('TOTAL')
  461. if diff_results is None:
  462. for k in fields:
  463. line.append(getattr(r, k).table()
  464. if getattr(r, k, None) is not None
  465. else types[k].none)
  466. elif percent:
  467. for k in fields:
  468. line.append(getattr(r, k).diff_table()
  469. if getattr(r, k, None) is not None
  470. else types[k].diff_none)
  471. else:
  472. for k in fields:
  473. line.append(getattr(diff_r, k).diff_table()
  474. if getattr(diff_r, k, None) is not None
  475. else types[k].diff_none)
  476. for k in fields:
  477. line.append(getattr(r, k).diff_table()
  478. if getattr(r, k, None) is not None
  479. else types[k].diff_none)
  480. for k in fields:
  481. line.append(types[k].diff_diff(
  482. getattr(r, k, None),
  483. getattr(diff_r, k, None)))
  484. if diff_results is None:
  485. line.append('')
  486. elif percent:
  487. line.append(' (%s)' % ', '.join(
  488. '+∞%' if t == +m.inf
  489. else '-∞%' if t == -m.inf
  490. else '%+.1f%%' % (100*t)
  491. for t in ratios))
  492. else:
  493. line.append(' (%s)' % ', '.join(
  494. '+∞%' if t == +m.inf
  495. else '-∞%' if t == -m.inf
  496. else '%+.1f%%' % (100*t)
  497. for t in ratios
  498. if t)
  499. if any(ratios) else '')
  500. lines.append(line)
  501. # find the best widths, note that column 0 contains the names and column -1
  502. # the ratios, so those are handled a bit differently
  503. widths = [
  504. ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1
  505. for w, i in zip(
  506. it.chain([23], it.repeat(7)),
  507. range(len(lines[0])-1))]
  508. # print our table
  509. for line in lines:
  510. print('%-*s %s%s' % (
  511. widths[0], line[0],
  512. ' '.join('%*s' % (w, x)
  513. for w, x in zip(widths[1:], line[1:-1])),
  514. line[-1]))
  515. def openio(path, mode='r'):
  516. if path == '-':
  517. if mode == 'r':
  518. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  519. else:
  520. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  521. else:
  522. return open(path, mode)
  523. def main(csv_paths, *,
  524. by=None,
  525. fields=None,
  526. defines=None,
  527. sort=None,
  528. **args):
  529. # separate out renames
  530. renames = list(it.chain.from_iterable(
  531. ((k, v) for v in vs)
  532. for k, vs in it.chain(by or [], fields or [])))
  533. if by is not None:
  534. by = [k for k, _ in by]
  535. if fields is not None:
  536. fields = [k for k, _ in fields]
  537. # figure out types
  538. types = {}
  539. for t in TYPES.keys():
  540. for k in args.get(t, []):
  541. if k in types:
  542. print("error: conflicting type for field %r?" % k)
  543. sys.exit(-1)
  544. types[k] = TYPES[t]
  545. # rename types?
  546. if renames:
  547. types_ = {}
  548. for new_k, old_k in renames:
  549. if old_k in types:
  550. types_[new_k] = types[old_k]
  551. types.update(types_)
  552. # figure out merge operations
  553. ops = {}
  554. for o in OPS.keys():
  555. for k in args.get(o, []):
  556. if k in ops:
  557. print("error: conflicting op for field %r?" % k)
  558. sys.exit(-1)
  559. ops[k] = OPS[o]
  560. # rename ops?
  561. if renames:
  562. ops_ = {}
  563. for new_k, old_k in renames:
  564. if old_k in ops:
  565. ops_[new_k] = ops[old_k]
  566. ops.update(ops_)
  567. # find CSV files
  568. paths = []
  569. for path in csv_paths:
  570. if os.path.isdir(path):
  571. path = path + '/*.csv'
  572. for path in glob.glob(path):
  573. paths.append(path)
  574. if not paths:
  575. print("error: no .csv files found in %r?" % csv_paths)
  576. sys.exit(-1)
  577. results = []
  578. for path in paths:
  579. try:
  580. with openio(path) as f:
  581. reader = csv.DictReader(f, restval='')
  582. for r in reader:
  583. # rename fields?
  584. if renames:
  585. # make a copy so renames can overlap
  586. r_ = {}
  587. for new_k, old_k in renames:
  588. if old_k in r:
  589. r_[new_k] = r[old_k]
  590. r.update(r_)
  591. results.append(r)
  592. except FileNotFoundError:
  593. pass
  594. # homogenize
  595. Result = infer(results,
  596. by=by,
  597. fields=fields,
  598. types=types,
  599. ops=ops,
  600. renames=renames)
  601. results_ = []
  602. for r in results:
  603. try:
  604. results_.append(Result(**{
  605. k: r[k] for k in Result._by + Result._fields
  606. if k in r and r[k].strip()}))
  607. except TypeError:
  608. pass
  609. results = results_
  610. # fold
  611. results = fold(Result, results, by=by, defines=defines)
  612. # sort, note that python's sort is stable
  613. results.sort()
  614. if sort:
  615. for k, reverse in reversed(sort):
  616. results.sort(key=lambda r: (getattr(r, k),)
  617. if getattr(r, k) is not None else (),
  618. reverse=reverse ^ (not k or k in Result._fields))
  619. # write results to CSV
  620. if args.get('output'):
  621. with openio(args['output'], 'w') as f:
  622. writer = csv.DictWriter(f, Result._by + Result._fields)
  623. writer.writeheader()
  624. for r in results:
  625. # note we need to go through getattr to resolve lazy fields
  626. writer.writerow({
  627. k: getattr(r, k) for k in Result._by + Result._fields})
  628. # find previous results?
  629. if args.get('diff'):
  630. diff_results = []
  631. try:
  632. with openio(args['diff']) as f:
  633. reader = csv.DictReader(f, restval='')
  634. for r in reader:
  635. # rename fields?
  636. if renames:
  637. # make a copy so renames can overlap
  638. r_ = {}
  639. for new_k, old_k in renames:
  640. if old_k in r:
  641. r_[new_k] = r[old_k]
  642. r.update(r_)
  643. try:
  644. diff_results.append(Result(**{
  645. k: r[k] for k in Result._by + Result._fields
  646. if k in r and r[k].strip()}))
  647. except TypeError:
  648. pass
  649. except FileNotFoundError:
  650. pass
  651. # fold
  652. diff_results = fold(Result, diff_results, by=by, defines=defines)
  653. # print table
  654. if not args.get('quiet'):
  655. table(Result, results,
  656. diff_results if args.get('diff') else None,
  657. by=by,
  658. fields=fields,
  659. sort=sort,
  660. **args)
  661. if __name__ == "__main__":
  662. import argparse
  663. import sys
  664. parser = argparse.ArgumentParser(
  665. description="Summarize measurements in CSV files.",
  666. allow_abbrev=False)
  667. parser.add_argument(
  668. 'csv_paths',
  669. nargs='*',
  670. default=CSV_PATHS,
  671. help="Description of where to find *.csv files. May be a directory "
  672. "or list of paths. Defaults to %r." % CSV_PATHS)
  673. parser.add_argument(
  674. '-q', '--quiet',
  675. action='store_true',
  676. help="Don't show anything, useful with -o.")
  677. parser.add_argument(
  678. '-o', '--output',
  679. help="Specify CSV file to store results.")
  680. parser.add_argument(
  681. '-d', '--diff',
  682. help="Specify CSV file to diff against.")
  683. parser.add_argument(
  684. '-a', '--all',
  685. action='store_true',
  686. help="Show all, not just the ones that changed.")
  687. parser.add_argument(
  688. '-p', '--percent',
  689. action='store_true',
  690. help="Only show percentage change, not a full diff.")
  691. parser.add_argument(
  692. '-b', '--by',
  693. action='append',
  694. type=lambda x: (
  695. lambda k,v=None: (k, v.split(',') if v is not None else ())
  696. )(*x.split('=', 1)),
  697. help="Group by this field. Can rename fields with new_name=old_name.")
  698. parser.add_argument(
  699. '-f', '--field',
  700. dest='fields',
  701. action='append',
  702. type=lambda x: (
  703. lambda k,v=None: (k, v.split(',') if v is not None else ())
  704. )(*x.split('=', 1)),
  705. help="Show this field. Can rename fields with new_name=old_name.")
  706. parser.add_argument(
  707. '-D', '--define',
  708. dest='defines',
  709. action='append',
  710. type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
  711. help="Only include results where this field is this value. May include "
  712. "comma-separated options.")
  713. class AppendSort(argparse.Action):
  714. def __call__(self, parser, namespace, value, option):
  715. if namespace.sort is None:
  716. namespace.sort = []
  717. namespace.sort.append((value, True if option == '-S' else False))
  718. parser.add_argument(
  719. '-s', '--sort',
  720. action=AppendSort,
  721. help="Sort by this fields.")
  722. parser.add_argument(
  723. '-S', '--reverse-sort',
  724. action=AppendSort,
  725. help="Sort by this fields, but backwards.")
  726. parser.add_argument(
  727. '-Y', '--summary',
  728. action='store_true',
  729. help="Only show the total.")
  730. parser.add_argument(
  731. '--int',
  732. action='append',
  733. help="Treat these fields as ints.")
  734. parser.add_argument(
  735. '--float',
  736. action='append',
  737. help="Treat these fields as floats.")
  738. parser.add_argument(
  739. '--frac',
  740. action='append',
  741. help="Treat these fields as fractions.")
  742. parser.add_argument(
  743. '--sum',
  744. action='append',
  745. help="Add these fields (the default).")
  746. parser.add_argument(
  747. '--prod',
  748. action='append',
  749. help="Multiply these fields.")
  750. parser.add_argument(
  751. '--min',
  752. action='append',
  753. help="Take the minimum of these fields.")
  754. parser.add_argument(
  755. '--max',
  756. action='append',
  757. help="Take the maximum of these fields.")
  758. parser.add_argument(
  759. '--mean',
  760. action='append',
  761. help="Average these fields.")
  762. parser.add_argument(
  763. '--stddev',
  764. action='append',
  765. help="Find the standard deviation of these fields.")
  766. parser.add_argument(
  767. '--gmean',
  768. action='append',
  769. help="Find the geometric mean of these fields.")
  770. parser.add_argument(
  771. '--gstddev',
  772. action='append',
  773. help="Find the geometric standard deviation of these fields.")
  774. sys.exit(main(**{k: v
  775. for k, v in vars(parser.parse_intermixed_args()).items()
  776. if v is not None}))