def show_target_info(target): columns: List[Column] = list() data_list: List[List[Any]] = list() columns.append( Column("", width=25, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT)) columns.append( Column("", width=80, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT)) st = SimpleTable(columns) output = "\nTarget Information" data_list.append(["model name", target.model_name]) data_list.append(["model data type", target.model_data_type]) data_list.append(["model endpoint", target.model_endpoint]) data_list.append(["model input shape", target.model_input_shape]) data_list.append([ f"model output classes ({len(target.model_output_classes)})", target.model_output_classes ]) if target.__doc__: data_list.append(["model docs", target.__doc__]) output += st.generate_table(data_list, row_spacing=0) + "\n" if target.active_attack: output += show_attack_info(target.active_attack) return output
def show_current_sample(target, samples, heading1=None, sample_index=None): columns: List[Column] = list() data_list: List[List[Any]] = list() if sample_index is None: sample_index = target.active_attack.sample_index if heading1 is None: heading1 = "Sample Index" # future support for multiple indices if not hasattr(sample_index, "__iter__"): sample_index = [sample_index] columns.append( Column( heading1, width=20, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Value", width=80, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT, )) for index, value in zip(sample_index, samples): data_list.append([index, value]) st = SimpleTable(columns) output = "\n" + st.generate_table(data_list, row_spacing=0) + "\n" return output
def show_attack_options(attack): columns: List[Column] = list() data_list: List[List[Any]] = list() # create structure to ensure all params are present params_struct = namedtuple( "params", [i for i in attack.default.keys()] + ["sample_index", "target_class"], defaults=list(attack.default.values()) + [0, 0], ) # get default parameters default_params = params_struct() if hasattr(attack, 'parameters'): # get current parameters current_params = {k: v for k, v in attack.parameters.items()} current_params["sample_index"] = attack.sample_index current_params["target_class"] = attack.target_class # ensure everything exists and is ordered correctly current_params = params_struct(**current_params)._asdict() columns.append( Column( "Attack Parameter (type)", width=25, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Default", width=12, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT, )) if hasattr(attack, 'parameters'): columns.append( Column( "Current", width=12, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT, )) st = SimpleTable(columns) for k, v in zip(default_params._fields, default_params): param = f"{k} ({str(type(v).__name__)})" default_value = v if hasattr(attack, 'parameters'): # active attack with current parameters? current_value = current_params.get(k, "") data_list.append([param, default_value, current_value]) else: data_list.append([param, default_value]) return st.generate_table(data_list, row_spacing=0) + "\n"
def list_frameworks(): columns: List[Column] = list() data_list: List[List[Any]] = list() columns.append(Column("Framework", width=20)) columns.append(Column("# of Attacks", width=30)) for framework, list_of_attacks in CFState.get_instance( ).loaded_frameworks.items(): data_list.append([framework, len(list_of_attacks)]) st = SimpleTable(columns) print() print(st.generate_table(data_list, row_spacing=0)) print()
def main(): # Default to terminal mode so redirecting to a file won't include the ANSI style sequences ansi.allow_style = ansi.STYLE_TERMINAL st = SimpleTable(columns) table = st.generate_table(data_list) ansi_print(table) bt = BorderedTable(columns) table = bt.generate_table(data_list) ansi_print(table) at = AlternatingTable(columns) table = at.generate_table(data_list) ansi_print(table)
def list_attacks(): columns: List[Column] = list() data_list: List[List[Any]] = list() columns.append(Column("Name", width=25)) columns.append(Column("Type", width=15)) columns.append(Column("Category", width=15)) columns.append(Column("Tags", width=15)) columns.append(Column("Framework", width=10)) for _, attack_obj in CFState.get_instance().loaded_attacks.items(): tags = ", ".join(attack_obj.tags) data_list.append([ attack_obj.attack_name, attack_obj.attack_type, attack_obj.category, tags, attack_obj.framework ]) st = SimpleTable(columns) print() print(st.generate_table(data_list, row_spacing=0)) print()
def list_targets(): columns: List[Column] = list() data_list: List[List[Any]] = list() columns.append(Column("Name", width=15)) columns.append(Column("Type", width=15)) columns.append(Column("Input Shape", width=15)) columns.append(Column("Location", width=85)) for _, target_obj in CFState.get_instance().loaded_targets.items(): shp = str(target_obj.model_input_shape) data_list.append([ target_obj.model_name, target_obj.model_data_type, shp, target_obj.model_endpoint ]) st = SimpleTable(columns) print() print(st.generate_table( data_list, row_spacing=0, )) print()
def show_attack_info(attack): columns: List[Column] = list() data_list: List[List[Any]] = list() columns.append( Column( "", width=25, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column("", width=80, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT)) st = SimpleTable(columns) data_list.append(["attack name", attack.attack_name]) data_list.append(["attack type", attack.attack_type]) data_list.append(["attack category", attack.category]) data_list.append(["attack tags", attack.tags]) data_list.append(["attack framework", attack.framework]) if attack.attack_cls.__doc__: data_list.append([ "attack docs", attack.attack_cls.__doc__.replace('\n', ' ').replace('\t', '').replace( ' ', ' ') ]) output = '\nAttack Information' + st.generate_table(data_list, row_spacing=0) + "\n" output += "\n" + show_attack_options(attack) return output
def test_simple_table_width(): # Base width for num_cols in range(1, 10): assert SimpleTable.base_width(num_cols) == (num_cols - 1) * 2 # Invalid num_cols value with pytest.raises(ValueError) as excinfo: SimpleTable.base_width(0) assert "Column count cannot be less than 1" in str(excinfo.value) # Total width column_1 = Column("Col 1", width=16) column_2 = Column("Col 2", width=16) row_data = list() row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) st = SimpleTable([column_1, column_2]) assert st.total_width() == 34
def do_predict(self, args): """Predict a single sample for the active target""" if CFState.get_instance().active_target is None: self.pwarning("\n [!] must first `interact` with a target.\n") return else: target = CFState.get_instance().active_target if sum([args.random, args.sample_index is not None, args.result]) > 1: self.pwarning("\n [!] must specify only one of {random, sample_index, result}.\n") return heading1 = "Sample Index" if args.random: sample_index = random.randint(0, len(target.X) - 1) samples = set_attack_samples(target, sample_index) elif args.sample_index: # default behavior sample_index = args.sample_index samples = set_attack_samples(target, sample_index) elif args.result: try: samples = target.active_attack.results['final']['input'] sample_index = [target.active_attack.attack_id] * len(samples) heading1 = "Attack ID" except (KeyError, AttributeError): self.pwarning("\n [!] No results found. First 'run' an attack.\n") return elif target.active_attack is not None and target.active_attack.sample_index is not None: sample_index = target.active_attack.sample_index samples = set_attack_samples(target, sample_index) else: self.pwarning("\n [!] No index sample, setting random index.\n") sample_index = random.randint(0, len(target.X) - 1) samples = set_attack_samples(target, sample_index) result = target._submit(samples) columns: List[Column] = list() columns.append( Column( heading1, width=8, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, ) ) columns.append( Column( "Sample", width=60, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, ) ) columns.append( Column( "Output Scores\n" + str(target.model_output_classes).replace(',', ''), width=30, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, ) ) if not hasattr(sample_index, "__iter__"): sample_index = [sample_index] samples_str = get_printable_batch(target, samples) results_str = printable_numpy(result) data_list: List[List[Any]] = list() for idx, samp, res in zip(sample_index, samples_str, results_str): data_list.append([idx, samp, res]) st = SimpleTable(columns) self.poutput("\n" + st.generate_table(data_list, row_spacing=0) + "\n")
def test_simple_table_creation(): column_1 = Column("Col 1", width=16) column_2 = Column("Col 2", width=16) row_data = list() row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) # Default options st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' '----------------------------------\n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # Custom divider st = SimpleTable([column_1, column_2], divider_char='─') table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' '──────────────────────────────────\n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # No divider st = SimpleTable([column_1, column_2], divider_char=None) table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # No row spacing st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, row_spacing=0) assert table == ('Col 1 Col 2 \n' '----------------------------------\n' 'Col 1 Row 1 Col 2 Row 1 \n' 'Col 1 Row 2 Col 2 Row 2 ') # No header st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, include_header=False) assert table == ('Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # Wide custom divider (divider needs no padding) st = SimpleTable([column_1, column_2], divider_char='深') table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' '深深深深深深深深深深深深深深深深深\n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # Wide custom divider (divider needs padding) column_2 = Column("Col 2", width=17) st = SimpleTable([column_1, column_2], divider_char='深') table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' '深深深深深深深深深深深深深深深深深 \n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # Invalid divider character with pytest.raises(TypeError) as excinfo: SimpleTable([column_1, column_2], divider_char='too long') assert "Divider character must be exactly one character long" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: SimpleTable([column_1, column_2], divider_char='\n') assert "Divider character is an unprintable character" in str(excinfo.value) # Invalid row spacing st = SimpleTable([column_1, column_2]) with pytest.raises(ValueError) as excinfo: st.generate_table(row_data, row_spacing=-1) assert "Row spacing cannot be less than 0" in str(excinfo.value)
def get_printable_scan_summary(summaries_by_attack, summaries_by_label=None): output = "\n =============== \n SCAN SUMMARY \n ===============\n\n" terminal_cols = os.get_terminal_size().columns results_width = terminal_cols - 128 # default Windows is 120x30 if results_width <= 0: output += bold_yellow( """\nIncrease terminal width to show parameters.\n\n""") columns: List[Column] = list() columns.append( Column( "Attack Name", width=15, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Total Runs", width=10, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Successes (%)", width=13, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Time[sec] (min/avg/max)", width=15, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Queries (min/avg/max)", width=18, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Best Score (attack_id)", width=15, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) if results_width > 0: columns.append( Column( "Best Parameters", width=25, header_horiz_align=HorizontalAlignment.RIGHT, data_horiz_align=HorizontalAlignment.RIGHT, )) data_list: List[List[Any]] = list() for name, summary in summaries_by_attack.items(): frac = summary["total_successes"] / summary["total_runs"] successes = f"{summary['total_successes']} ({frac:>.1%})" times = f"{summary['min_time']:>4.1f}/{summary['avg_time']:>4.1f}/{summary['max_time']:>4.1f}" queries = f"{summary['min_queries']:>5d}/{summary['avg_queries']:>5d}/{summary['max_queries']:>5d}" best = ( f"{summary['best_attack_score']:0.1f} ({summary['best_attack_id']})" if summary["best_attack_score"] else "N/A") if results_width > 0: if summary["best_params"] is not None: trunc_params = shallow_dict_to_fixed_width( (summary["best_params"])) param_str = json.dumps(trunc_params, indent=1, separators=("", "="))[2:-1].replace( '"', "") else: param_str = "N/A" data_list.append([ name, summary["total_runs"], successes, times, queries, best, param_str ]) else: data_list.append( [name, summary["total_runs"], successes, times, queries, best]) st = SimpleTable(columns) output += '\n' + st.generate_table(data_list, row_spacing=0) + '\n' if summaries_by_label is not None: output += "\n" # table by sample_index columns: List[Column] = list() columns.append( Column( "Class Label", width=15, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Total Runs", width=10, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Successes (%)", width=13, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Time[sec] (min/avg/max)", width=15, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Queries (min/avg/max)", width=18, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Best Score (Attack)", width=15, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT, )) data_list: List[List[Any]] = list() for name, summary in sorted(summaries_by_label.items()): frac = summary["total_successes"] / summary["total_runs"] successes = f"{summary['total_successes']} ({frac:>.1%})" times = f"{summary['min_time']:>4.1f}/{summary['avg_time']:>4.1f}/{summary['max_time']:>4.1f}" queries = f"{summary['min_queries']:>5d}/{summary['avg_queries']:>5d}/{summary['max_queries']:>5d}" best = ( f"{summary['best_attack_score']:0.1f} ({summary['best_attack_name']})" if summary["best_attack_score"] else "N/A") data_list.append( [name, summary["total_runs"], successes, times, queries, best]) st = SimpleTable(columns) output += '\n' + st.generate_table(data_list, row_spacing=0) + '\n' return output
def get_printable_run_summary(summary): output = "" output += f"\n[+] {summary['successes']}/{summary['batch_size']} succeeded\n\n" if summary['elapsed_time'] > summary['queries']: query_rate = summary['elapsed_time'] / summary['queries'] units = 'sec/query' else: query_rate = summary["queries"] / summary["elapsed_time"] units = "query/sec" metric = summary["input_change_metric"] terminal_cols = os.get_terminal_size().columns results_width = terminal_cols - 125 # default Windows is 120x30 columns: List[Column] = list() columns.append(Column("", width=3)) # number columns.append( Column( "Sample Index", width=13, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Label (conf)", width=18, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Attack Label (conf)", width=19, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( metric, width=len(metric), header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Elapsed Time [sec]", width=18, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) columns.append( Column( "Queries (rate)", width=18, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.RIGHT, )) if results_width > 0: columns.append( Column( "Attack Input", width=results_width, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT, )) data_list: List[List[Any]] = list() elapsed_time_str = f"{summary['elapsed_time']:.1f}" query_rate_str = f"{summary['queries']:.0f} ({query_rate:.1f} {units})" for i, (si, li, conf_0, lf, conf_f, change, res, d) in enumerate( zip(summary["sample_index"], summary["initial_label"], summary["initial_confidence"], summary["final_label"], summary["final_confidence"], summary["input_change"], summary["result"], summary["degenerate"])): label_confidence = f"{li} ({conf_0:.4f})" final_confidence = f"{lf} ({conf_f:.4f})" if d: label_confidence = f"{bold_yellow('*')} " + label_confidence final_confidence = f"{bold_yellow('*')} " + final_confidence change_str = f"{change:.5%}" if results_width > 0: data_list.append([ f"{i+1}.", si, label_confidence, final_confidence, change_str, elapsed_time_str, query_rate_str, str(np.array(res)), ]) else: data_list.append([ f"{i+1}.", si, label_confidence, final_confidence, change_str, elapsed_time_str, query_rate_str, ]) if sum(summary["degenerate"]) > 0: output += bold_yellow( " * target_class is the same as the original class") + "\n\n" if results_width <= 0: output = bold_yellow( """\nIncrease terminal width to show results.\n""") + output # return table as output st = SimpleTable(columns) return output + '\n' + st.generate_table(data_list, row_spacing=0) + '\n'
def do_set(self, args): """Set parameters of the active attack on the active target using param1=val1 param2=val2 notation. This command replaces built-in "set" command, which is renamed to "setg". """ if not CFState.get_instance().active_target: self.pwarning( '\n [!] No active target. Try "setg" for setting global arguments.\n' ) return if not CFState.get_instance().active_target.active_attack: self.pwarning('\n [!] No active attack. Try "use <attack>".\n') return # 'set' with no options shows current variables, similar to "show options" if not args.what: self.pwarning( '\n [!] No arguments specified. Try "set <param>=<value>".\n') # create structure to ensure all params are present and ordered properly. Defaults to current params to prevent over writing with default values params_struct = namedtuple( "params", [ i for i in CFState.get_instance().active_target.active_attack. parameters.keys() ] + ["sample_index", "target_class"], defaults=list(CFState.get_instance( ).active_target.active_attack.parameters.values()) + [ CFState.get_instance().active_target.active_attack.sample_index, CFState.get_instance().active_target.active_attack.target_class, ], ) # parse parameters and new values from the args try: params_to_update = re.findall(r"(\w+)\s?=\s?([\w\.]+)", " ".join(args.what)) except: self.pwarning("\n [!] Failed to parse arguments.\n") return # create default params struct default_params = { k: v for k, v in CFState.get_instance().active_target.active_attack.default.items() } default_params["sample_index"] = CFState.get_instance( ).active_target.active_attack.sample_index default_params["target_class"] = CFState.get_instance( ).active_target.active_attack.target_class # ensure all current params exist and are ordered correctly default_params = params_struct(**default_params)._asdict() # convert string "True"/"true" and "False"/"false" to boolean for i, v in enumerate(params_to_update): if type(default_params.get(v[0], None)) is bool: if v[1].lower() == "true" or int(v[1]) == 1: params_to_update[i] = (v[0], True) elif v[1].lower() == "false" or int(v[1]) == 0: params_to_update[i] = (v[0], False) # create new params struct using default values where no new values are spec'd new_params = params_struct(**{ i[0]: type(default_params.get(i[0], ""))(i[1]) for i in params_to_update }) # create current params struct current_params = { k: v for k, v in CFState.get_instance().active_target.active_attack.parameters.items() } current_params["sample_index"] = CFState.get_instance( ).active_target.active_attack.sample_index current_params["target_class"] = CFState.get_instance( ).active_target.active_attack.target_class # ensure all current params exist and are ordered correctly current_params = params_struct(**current_params)._asdict() # separate target_class and sample_index from struct and update the relevant values CFState.get_instance().active_target.active_attack.parameters = { k: v for k, v in zip(new_params._fields[:-2], new_params[:-2]) } CFState.get_instance( ).active_target.active_attack.sample_index = new_params.sample_index CFState.get_instance( ).active_target.active_attack.target_class = new_params.target_class # print info print_new_params = new_params._asdict() columns: List[Column] = list() data_list: List[List[Any]] = list() columns.append( Column("Attack Parameter (type)", width=25, header_horiz_align=HorizontalAlignment.LEFT, data_horiz_align=HorizontalAlignment.RIGHT)) columns.append( Column("Default", width=12, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT)) columns.append( Column("Previous", width=12, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT)) columns.append( Column("New", width=12, header_horiz_align=HorizontalAlignment.CENTER, data_horiz_align=HorizontalAlignment.LEFT)) for k, default_value in default_params.items(): param = f"{k} ({str(type(default_value).__name__)})" previous_value = current_params.get(k, "") new_value = print_new_params.get(k, "") if new_value != previous_value: data_list.append([ param, str(default_value), str(previous_value), str(new_value) ]) else: data_list.append( [param, str(default_value)[:11], str(previous_value)[:11], ""]) st = SimpleTable(columns) print() print(st.generate_table(data_list, row_spacing=0)) print()
def test_simple_table(): column_1 = Column("Col 1", width=15) column_2 = Column("Col 2", width=15) row_data = list() row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) # Default options st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' '--------------------------------\n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 \n') # Custom divider st = SimpleTable([column_1, column_2], divider_char='─') table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' '────────────────────────────────\n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 \n') # No divider st = SimpleTable([column_1, column_2], divider_char=None) table = st.generate_table(row_data) assert table == ('Col 1 Col 2 \n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 \n') # No row spacing st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, row_spacing=0) assert table == ('Col 1 Col 2 \n' '--------------------------------\n' 'Col 1 Row 1 Col 2 Row 1 \n' 'Col 1 Row 2 Col 2 Row 2 \n') # No header st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, include_header=False) assert table == ('Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 \n') # Invalid row spacing st = SimpleTable([column_1, column_2]) with pytest.raises(ValueError) as excinfo: st.generate_table(row_data, row_spacing=-1) assert "Row spacing cannot be less than 0" in str(excinfo.value)