Coverage for /Users/martin/prj/git/benchman_pre/src/benchman/reporter.py: 0%

74 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-12-24 08:16 +0100

1"""Convert Datasets into human-readable representation.""" 

2 

3from abc import ABC, abstractmethod 

4from io import TextIOBase 

5from pathlib import Path 

6 

7import tablib 

8 

9from benchman.dataset import Dataset 

10from benchman.util import FileOrStdout, logger 

11 

12 

13class Reporter(ABC): 

14 """Abstract class for reporting Datasets.""" 

15 

16 def __init__(self, dataset: Dataset): 

17 self.dataset: Dataset = dataset 

18 

19 @abstractmethod 

20 def report(self, *, out: Path | str | TextIOBase | None, **kwargs) -> None: 

21 """Return a human-readable representation of the Dataset.""" 

22 

23 def get_description_info(self) -> dict[str, str | list[str]]: 

24 return self.dataset.get_description_info() 

25 

26 def _mock_class_suffix(self, classes: set[str]) -> str: 

27 """Return a string representation of the classes.""" 

28 # return f" ({','.join(classes)})" 

29 cl = [] 

30 for c in classes: 

31 s = "" 

32 if c.startswith("name-"): 

33 s += "n" 

34 # continue 

35 elif c.startswith("variant-"): 

36 s += "v" 

37 continue 

38 elif c.startswith("row-"): 

39 s += "r" 

40 

41 if c.endswith("-best"): 

42 s += "++" 

43 elif c.endswith("-good"): 

44 s += "+" 

45 elif c.endswith("-bad"): 

46 s += "-" 

47 elif c.endswith("-worst"): 

48 s += "--" 

49 

50 if s: 

51 cl.append(s) 

52 

53 if cl: 

54 return f' ({",".join(cl)})' 

55 return "" 

56 

57 def to_tablib(self) -> tablib.Dataset: 

58 tds = tablib.Dataset( 

59 title=self.dataset.name, 

60 headers=self.dataset.header_titles, 

61 ) 

62 for row in self.dataset.rows: 

63 # cells = [f"{c.value}{self._mock_class_suffix(c.classes)}" for c in row] 

64 # cells = [f"{c.value} ({','.join(c.classes)})" for c in row] 

65 cells = [c.value for c in row] 

66 try: 

67 tds.append(cells) 

68 except Exception as e: 

69 logger.error(f"Error {e!r} adding row: {cells}") 

70 logger.error(f"{tds.headers=}") 

71 raise 

72 return tds 

73 

74 

75class TablibReporter(Reporter): 

76 """Text representation of a Dataset.""" 

77 

78 # See https://tablib.readthedocs.io/en/stable/formats.html 

79 # fmt: off 

80 cli_formats = [ 

81 "fancy_grid", "github", "grid", "html", "jira", "latex_booktabs", 

82 "latex_raw", "latex", "mediawiki", "moinmoin", "orgtbl", "pipe", "plain", 

83 "presto", "psql", "rst", "simple", "textile", "tsv", "youtrack", 

84 ] 

85 format_map = { 

86 "html": "cli.html", 

87 "markdown": "cli.pipe", 

88 "csv": "cli.csv", 

89 "json": "cli.json", 

90 "yaml": "cli.yaml", 

91 "df": "cli.df", 

92 } 

93 # fmt: on 

94 

95 def report( 

96 self, 

97 *, 

98 format: str = "cli.pipe", 

99 out: Path | str | TextIOBase | None = None, 

100 **kwargs, 

101 ) -> None: 

102 """Write a human-readable representation of the Dataset. 

103 

104 The heavy lifting is done by `tablib.Dataset.export()`. 

105 """ 

106 format = self.format_map.get(format, format) 

107 

108 export, format = format.split(".", 1) 

109 

110 assert export == "cli", f"Invalid format: {format}" 

111 assert format in self.cli_formats, f"Invalid cli format: {format}" 

112 

113 info = self.get_description_info() 

114 

115 tds = self.to_tablib() 

116 

117 if isinstance(out, (Path, str)): 

118 logger.info(f"Writing to {Path(out).absolute()}") 

119 

120 with FileOrStdout(out) as file: 

121 

122 def wl(s: str = "") -> None: 

123 file.write(f"{s}\n") 

124 

125 wl(f"\n# {info["title"]}") 

126 

127 if info["subtitle"]: 

128 wl("\n> " + "\n> ".join(info["subtitle"])) 

129 wl() 

130 

131 # The table itself: 

132 wl(str(tds.export(export, tablefmt=format))) 

133 

134 wl() 

135 for s in info["legend"]: 

136 wl(s) 

137 

138 for s in info["warnings"] or []: 

139 wl(s) 

140 wl()