def _wttab_eventcount(f, res, ec, count_filter, desclen, descfrm, caselen): # extrema count n = len(res.cases) f.write(f"Extrema Count\nFilter: {count_filter}\n\n") widths = [desclen, *([caselen] * n)] headers = ["Description", *res.cases] for j, frm, lbl in zip( (0, 1), (f"{{:{caselen}d}}", f"{{:{caselen}.1f}}"), ("Count", "Percent") ): formats = [descfrm, *([frm] * n)] hu_, frm_ = writer.formheader( headers, widths, formats, sep=[7, 1], just="c", ulchar="=" ) f.write(hu_) rowlabels, table = _rowlbls_table(lbl, ec, j) writer.vecwrite(f, frm_, rowlabels, table) if j == 0: f.write("\n")
def rptpct1( mxmn1, mxmn2, filename, *, title="PERCENT DIFFERENCE REPORT", names=("Self", "Reference"), desc=None, filterval=None, labels=None, units=None, ignorepv=None, uf_reds=None, use_range=True, numform=None, prtbad=None, prtbadh=None, prtbadl=None, flagbad=None, flagbadh=None, flagbadl=None, dohistogram=True, histogram_inc=1.0, domagpct=True, magpct_options=None, doabsmax=False, shortabsmax=False, roundvals=-1, rowhdr="Row", deschdr="Description", maxhdr="Maximum", minhdr="Minimum", absmhdr="Abs-Max", perpage=-1, tight_layout_args=None, show_figures=False, align_by_label=True, ): """ Write a percent difference report between 2 sets of max/min data Parameters ---------- mxmn1 : 2d array_like or SimpleNamespace The max/min data to compare to the `mxmn2` set. If 2-column array_like, its columns are: [max, min]. If SimpleNamespace, it must be as defined in :class:`DR_Results` and have these members: .. code-block:: none .ext = [max, min] .drminfo = SimpleNamespace which has (at least): .desc = one line description of category .filterval = the filter value; (see `filterval` description below) .labels = a list of descriptions; one per row .ignorepv = these rows will get 'n/a' for % diff .units = string with units .uf_reds = uncertainty factors Note that the inputs `desc`, `labels`, etc, override the values above. mxmn2 : 2d array_like or SimpleNamespace The reference set of max/min data. Format is the same as `mxmn1`. .. note:: If both `mxmn1` and `mxmn2` are SimpleNamespaces and have the ``.drminfo.labels`` attribute, this routine will, by default, use the labels to align the data sets for comparison. To prevent this, set the `align_by_label` parameter to False. filename : string or file_like or 1 or None Either a name of a file, or is a file_like object as returned by :func:`open` or :class:`io.StringIO`. Input as integer 1 to write to stdout. Can also be the name of a directory or None; in these cases, a GUI is opened for file selection. title : string; must be named; optional Title for the report names : list/tuple; must be named; optional Two (short) strings identifying the two sets of data desc : string or None; must be named; optional A one line description of the table. Overrides `mxmn1.drminfo.desc`. If neither are input, 'No description provided' is used. filterval : scalar, 1d array_like or None; must be named; optional Numbers with absolute value <= than `filterval` will get a 'n/a' % diff. If vector, length must match number of rows in `mxmn1` and `mxmn2` data. Overrides `mxmn1.drminfo.filterval`. If neither are input, `filterval` is set to 1.e-6. labels : list or None; must be named; optional A list of strings briefly describing each row. Overrides `mxmn1.drminfo.labels`. If neither are input, ``['Row 1','Row 2',...]`` is used. units : string or None; must be named; optional Specifies the units. Overrides `mxmn1.drminfo.units`. If neither are input, 'Not specified' is used. ignorepv : 1d array or None; must be named; optional 0-offset index vector specifying which rows of `mxmn1` to ignore (they get the 'n/a' % diff). Overrides `mxmn1.drminfo.ignorepv`. If neither are input, no rows are ignored (though `filterval` is still used). .. note:: `ignorepv` applies *before* any alignment by labels is done (when `align_by_label` is True, which is the default). uf_reds : 1d array or None; must be named; optional Uncertainty factors: [rigid, elastic, dynamic, static]. Overrides `mxmn1.drminfo.uf_reds`. If neither is input, 'Not specified' is used. use_range : bool; must be named, optional If True, the denominator of the % diff calc for both the max & min for each row is the absolute maximum of the reference max & min for that row. If False, the denominator is the applicable reference max or min. A quick example shows why ``use_range=True`` might be useful: .. code-block:: none If [max1, min1] = [12345, -10] and [max2, min2] = [12300, 50] Then: % diff = [0.37%, 0.49%] if use_range is True % diff = [0.37%, 120.00%] if use_range is False Note that the sign of the % diff is defined such that a positive % diff means an exceedance: where ``max1 > max2`` or ``min1 < min2``. `use_range` is ignored if `doabsmax` is True. numform : string or None; must be named; optional Format of the max & min numbers. If None, it is set internally to be 13 chars wide and depends on the range of numbers to print: - if range is "small", numform='{:13.xf}' where "x" ranges from 0 to 7 - if range is "large", numform='{:13.6e}' prtbad : scalar or None; must be named; optional Only print rows where ``abs(%diff) > prtbad``. For example, to print rows off by more than 5%, use ``prtbad=5``. `prtbad` takes precedence over `prtbadh` and `prtbadl`. prtbadh : scalar or None; must be named; optional Only print rows where ``%diff > prtbadh``. Handy for showing just the exceedances. `prtbadh` takes precedence over `prtbadl`. prtbadl : scalar or None; must be named; optional Only print rows where ``%diff < prtbadl``. Handy for showing where reference rows are higher. flagbad : scalar or None; must be named; optional Flag % diffs where ``abs(%diff) > flagbad``. Works similar to `prtbad`. The flag is an asterisk (*). flagbadh : scalar or None; must be named; optional Flag % diffs where ``%diff > flagbadh``. Works similar to `prtbadh`. Handy for flagging exceedances. `flagbadh` takes precedence over `flagbadl`. flagbadl : scalar or None; must be named; optional Flag % diffs where ``%diff < flagbadl``. Works similar to `prtbadl`. dohistogram : bool; must be named; optional If True, plot the histograms. Plots will be written to "`filename`.histogram.png". histogram_inc : scalar; must be named; optional The histogram increment; defaults to 1.0 (for 1%). domagpct : bool; must be named; optional If True, plot the percent differences versus magnitude via :func:`magpct`. Plots will be written to "`filename`.magpct.png". Filtering for the "magpct" plot is controlled by the ``magpct_options['filterval']`` and ``magpct_options['symlogy']`` options. By default, all percent differences are shown, but the larger values (according to the `filterval` filter) are emphasized by using a mixed linear/log y-axis. The percent differences for the `ignorepv` rows are not plotted. magpct_options : None or dict; must be named; optional If None, it is internally reset to:: magpct_options = {'filterval': 'filterval'} Use this parameter to provide any options to :func:`magpct` but note that the `filterval` option for :func:`magpct` is treated specially. Here, in addition to any of the values that :func:`magpct` accepts, it can also be set to the string "filterval" as in the default case shown above. In that case, ``magpct_options['filterval']`` gets internally reset to the initial value of `filterval` (which is None by default). .. note:: The call to :func:`magpct` is *after* applying `ignorepv` and doing any data aligning by labels. .. note:: The two filter value options (`filterval` and ``magpct_options['filterval']``) have different defaults: None and 'filterval`, respectively. They also differ on how the ``None`` setting is used: for `filterval`, None is replaced by 1.e-6 while for `magpct_filterval`, None means that the "magpct" plot will not have any filters applied at all. .. note:: The above means that, if you accept the default values for `filterval` and for ``magpct_options['filterval']``, then tables and the histogram plots will use a `filterval` of 1.e-6 while the "magpct" plots will use no filter (it compares everything except perfect zeros). doabsmax : bool; must be named; optional If True, compare only absolute maximums. shortabsmax : bool; must be named; optional If True, set ``doabsmax=True`` and do not print the max1 and min1 columns. roundvals : integer; must be named; optional Round max & min numbers at specified decimal. If negative, no rounding. rowhdr : string; must be named; optional Header for row number column deschdr : string; must be named; optional Header for description column maxhdr : string; must be named; optional Header for the column 1 data minhdr : string; must be named; optional Header for the column 2 data absmhdr : string; must be named; optional Header for abs-max column perpage : integer; must be named; optional The number of lines to write perpage. If < 1, there is no limit (one page). tight_layout_args : dict or None; must be named; optional Arguments for :func:`matplotlib.pyplot.tight_layout`. If None, defaults to ``{'pad': 3.0}``. show_figures : bool; must be named; optional If True, plot figures will be displayed on the screen for interactive viewing. Warning: there may be many figures. align_by_label : bool; must be named; optional If True, use labels to align the two sets of data for comparison. See note above under the `mxmn2` option. Returns ------- pdiff_info : dict Dictionary with 'amx' (abs-max), 'mx' (max), and 'mn' keys: .. code-block:: none <class 'dict'>[n=3] 'amx': <class 'dict'>[n=5] 'hsto' : float64 ndarray 33 elems: (11, 3) 'mag' : [n=2]: (float64 ndarray: (100,), ... 'pct' : float64 ndarray 100 elems: (100,) 'prtpv': bool ndarray 100 elems: (100,) 'spct' : [n=100]: [' -2.46', ' -1.50', ... 'mn' : <class 'dict'>[n=5] 'hsto' : float64 ndarray 33 elems: (11, 3) 'mag' : [n=2]: (float64 ndarray: (100,), ... 'pct' : float64 ndarray 100 elems: (100,) 'prtpv': bool ndarray 100 elems: (100,) 'spct' : [n=100]: [' 1.55', ' 1.53', ... 'mx' : <class 'dict'>[n=5] 'hsto' : float64 ndarray 27 elems: (9, 3) 'mag' : [n=2]: (float64 ndarray: (100,), ... 'pct' : float64 ndarray 100 elems: (100,) 'prtpv': bool ndarray 100 elems: (100,) 'spct' : [n=100]: [' -2.46', ' -1.50', ... Where: .. code-block:: none 'hsto' : output of :func:`histogram`: [center, count, %] 'mag' : inputs to :func:`magpct` 'pct' : percent differences 'prtpv' : rows to print partition vector 'spct' : string version of 'pct' Examples -------- >>> import numpy as np >>> from pyyeti import cla >>> ext1 = [[120.0, -8.0], ... [8.0, -120.0]] >>> ext2 = [[115.0, -5.0], ... [10.0, -125.0]] Run :func:`rptpct1` multiple times to get a more complete picture of all the output (the table is very wide). Also, the plots will be turned off for this example. First, the header: >>> opts = {'domagpct': False, 'dohistogram': False} >>> dct = cla.rptpct1(ext1, ext2, 1, **opts) # doctest: +ELLIPSIS PERCENT DIFFERENCE REPORT <BLANKLINE> Description: No description provided Uncertainty: Not specified Units: Not specified Filter: 1e-06 Notes: % Diff = +/- abs(Self-Reference)/max(abs(Reference... Sign set such that positive % differences indicate... Date: ... ... Then, the max/min/absmax percent difference table in 3 calls: >>> dct = cla.rptpct1(ext1, ext2, 1, **opts) # doctest: +ELLIPSIS PERCENT DIFFERENCE REPORT ... Self Reference ... Row Description Maximum Maximum % Diff ... ------- ----------- ------------- ------------- ------- ... 1 Row 1 120.00000 115.00000 4.35 ... 2 Row 2 8.00000 10.00000 -1.60 ... ... >>> dct = cla.rptpct1(ext1, ext2, 1, **opts) # doctest: +ELLIPSIS PERCENT DIFFERENCE REPORT ... ... Self Reference ... Row Description ... Minimum Minimum % Diff ... ------- ----------- ...------------- ------------- ------- ... 1 Row 1 ... -8.00000 -5.00000 2.61 ... 2 Row 2 ... -120.00000 -125.00000 -4.00 ... ... >>> dct = cla.rptpct1(ext1, ext2, 1, **opts) # doctest: +ELLIPSIS PERCENT DIFFERENCE REPORT ... ... Self Reference Row Description ... Abs-Max Abs-Max % Diff ------- ----------- ...------------- ------------- ------- 1 Row 1 ... 120.00000 115.00000 4.35 2 Row 2 ... 120.00000 125.00000 -4.00 ... Finally, the histogram summaries: >>> dct = cla.rptpct1(ext1, ext2, 1, **opts) # doctest: +ELLIPSIS PERCENT DIFFERENCE REPORT ... No description provided - Maximum Comparison Histogram <BLANKLINE> % Diff Count Percent -------- -------- ------- -2.00 1 50.00 4.00 1 50.00 <BLANKLINE> 0.0% of values are within 1% 50.0% of values are within 2% 100.0% of values are within 5% <BLANKLINE> % Diff Statistics: [Min, Max, Mean, StdDev] = [-1.60, 4.35,... <BLANKLINE> <BLANKLINE> No description provided - Minimum Comparison Histogram <BLANKLINE> % Diff Count Percent -------- -------- ------- -4.00 1 50.00 3.00 1 50.00 <BLANKLINE> 0.0% of values are within 1% 100.0% of values are within 5% <BLANKLINE> % Diff Statistics: [Min, Max, Mean, StdDev] = [-4.00, 2.61,... <BLANKLINE> <BLANKLINE> No description provided - Abs-Max Comparison Histogram <BLANKLINE> % Diff Count Percent -------- -------- ------- -4.00 1 50.00 4.00 1 50.00 <BLANKLINE> 0.0% of values are within 1% 100.0% of values are within 5% <BLANKLINE> % Diff Statistics: [Min, Max, Mean, StdDev] = [-4.00, 4.35,... """ if tight_layout_args is None: tight_layout_args = {"pad": 3.0} if magpct_options is None: magpct_options = {"filterval": "filterval"} else: magpct_options = magpct_options.copy() # magpct_options['filterval'] get special treatment: magpct_filterval = magpct_options["filterval"] del magpct_options["filterval"] if isinstance(magpct_filterval, str): if magpct_filterval != "filterval": raise ValueError("``magpct_options['filterval']`` is an invalid " f"string: {magpct_filterval!r} (can only " "be 'filterval' if a string)") # copy the initial `filterval` setting: magpct_filterval = filterval infovars = ( "desc", "filterval", "magpct_filterval", "labels", "units", "ignorepv", "uf_reds", ) dct = locals() infodct = {n: dct[n] for n in infovars} del dct # check mxmn1: if isinstance(mxmn1, SimpleNamespace): sns = mxmn1.drminfo for key, value in infodct.items(): if value is None: infodct[key] = getattr(sns, key, None) del sns mxmn1 = mxmn1.ext else: mxmn1 = np.atleast_2d(mxmn1) row_number = np.arange(1, mxmn1.shape[0] + 1) # check mxmn2: if isinstance(mxmn2, SimpleNamespace) and getattr(mxmn2, "drminfo", None): labels2 = mxmn2.drminfo.labels mxmn2 = mxmn2.ext if align_by_label: # use labels and labels2 to align data; this is in case # the two sets of results recover some of the same items, # but not all mxmn1, mxmn2, row_number = _align_mxmn(mxmn1, mxmn2, labels2, row_number, infodct) else: mxmn2 = np.atleast_2d(mxmn2) desc = infodct["desc"] if desc is None: desc = "No description provided" R = mxmn1.shape[0] if R != mxmn2.shape[0]: raise ValueError( f"`mxmn1` and `mxmn2` have a different number of rows: " f"{R} vs {mxmn2.shape[0]} for category with `desc` = {desc}") filterval = infodct["filterval"] magpct_filterval = infodct["magpct_filterval"] labels = infodct["labels"] units = infodct["units"] ignorepv = infodct["ignorepv"] uf_reds = infodct["uf_reds"] del infodct if filterval is None: filterval = 1.0e-6 filterval = _proc_filterval(filterval, R, "filterval") magpct_filterval = _proc_filterval(magpct_filterval, R, "magpct_options['filterval']") if labels is None: labels = [f"Row {i + 1:6d}" for i in range(R)] elif len(labels) != R: raise ValueError("length of `labels` does not match number" f" of rows in `mxmn1`: {len(labels)} vs {R} for " f"category with `desc` = {desc}") if units is None: units = "Not specified" if numform is None: numform = _get_numform(mxmn1) pdhdr = "% Diff" nastring = "n/a " comppv = np.ones(R, bool) if ignorepv is not None: comppv[ignorepv] = False # for row labels: w = max(11, len(max(labels, key=len))) frm = f"{{:{w}}}" # start preparing for writer.formheader: print_info = SimpleNamespace( headers1=["", ""], headers2=[rowhdr, deschdr], formats=["{:7d}", frm], printargs=[row_number, labels], widths=[7, w], seps=[0, 2], justs=["c", "l"], ) if shortabsmax: doabsmax = True if doabsmax: use_range = False if roundvals > -1: mxmn1 = np.round(mxmn1, roundvals) mxmn2 = np.round(mxmn2, roundvals) prtbads = (prtbad, prtbadh, prtbadl) flagbads = (flagbad, flagbadh, flagbadl) # compute percent differences pctinfo = {} kwargs = dict( names=names, mxmn1=mxmn1, comppv=comppv, histogram_inc=histogram_inc, numform=numform, prtbads=prtbads, flagbads=flagbads, maxhdr=maxhdr, minhdr=minhdr, absmhdr=absmhdr, pdhdr=pdhdr, nastring=nastring, doabsmax=doabsmax, shortabsmax=shortabsmax, print_info=print_info, ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", r"All-NaN (slice|axis) encountered") mx1 = np.nanmax(abs(mxmn1), axis=1) mx2 = np.nanmax(abs(mxmn2), axis=1) if not doabsmax: max1, min1 = mxmn1[:, 0], mxmn1[:, 1] max2, min2 = mxmn2[:, 0], mxmn2[:, 1] mxmn_b = mxmn2 if use_range else None prtpv = np.zeros(R, bool) for i in zip( ("mx", "mn", "amx"), (max1, min1, mx1), (max2, min2, mx2), (True, False, True), (maxhdr, minhdr, absmhdr), ): lbl, ext1, ext2, ismax, valhdr = i pctinfo[lbl] = _proc_pct( ext1, ext2, filterval, magpct_filterval, mxmn_b=mxmn_b, ismax=ismax, valhdr=valhdr, **kwargs, ) prtpv |= pctinfo[lbl]["prtpv"] prtpv &= comppv else: pctinfo["amx"] = _proc_pct( mx1, mx2, filterval, magpct_filterval, mxmn_b=None, ismax=True, valhdr=absmhdr, **kwargs, ) prtpv = pctinfo["amx"]["prtpv"] hu, frm = writer.formheader( [print_info.headers1, print_info.headers2], print_info.widths, print_info.formats, sep=print_info.seps, just=print_info.justs, ) # format page header: misc = _get_filtline(filterval) + _get_noteline(use_range, names, prtbads, flagbads) hdrs = _get_rpt_headers(desc=desc, uf_reds=uf_reds, units=units, misc=misc) header = title + "\n\n" + hdrs + "\n" imode = plt.isinteractive() plt.interactive(show_figures) try: if domagpct: _plot_magpct( pctinfo, names, desc, doabsmax, filename, magpct_options, use_range, maxhdr, minhdr, absmhdr, show_figures, tight_layout_args, ) if dohistogram: _plot_histogram( pctinfo, names, desc, doabsmax, filename, histogram_inc, maxhdr, minhdr, absmhdr, show_figures, tight_layout_args, ) finally: plt.interactive(imode) # write results @guitools.write_text_file def _wtcmp(f, header, hu, frm, printargs, perpage, prtpv, pctinfo, desc): prtpv = prtpv.nonzero()[0] if perpage < 1: # one additional in case size is zero perpage = prtpv.size + 1 pages = (prtpv.size + perpage - 1) // perpage if prtpv.size < len(printargs[0]): for i, item in enumerate(printargs): printargs[i] = [item[j] for j in prtpv] tabhead = header + hu pager = "\n" # + chr(12) for p in range(pages): if p > 0: f.write(pager) f.write(tabhead) b = p * perpage e = b + perpage writer.vecwrite(f, frm, *printargs, so=slice(b, e)) f.write(pager) for lbl, hdr in zip(("mx", "mn", "amx"), (maxhdr, minhdr, absmhdr)): if lbl in pctinfo: f.write(_get_histogram_str(desc, hdr, pctinfo[lbl])) _wtcmp(filename, header, hu, frm, print_info.printargs, perpage, prtpv, pctinfo, desc) return pctinfo
def rpttab1(res, filename, title, count_filter=1e-6, name=None): """ Write results tables with bin count information. Parameters ---------- res : SimpleNamespace Results data structure with attributes `.ext`, `.cases`, `.drminfo`, etc (see example in :class:`DR_Results`) filename : string or file_like or 1 or None If a string that ends with '.xlsx', a Microsoft Excel file is written. Otherwise, `filename` is either a name of a file, or is a file_like object as returned by :func:`open` or :class:`io.StringIO`. Input as integer 1 to write to stdout. Can also be the name of a directory or None; in these cases, a GUI is opened for file selection. title : string Title for report count_filter : scalar; optional Filter to use for the bin count; only numbers larger (in the absolute value sense) than the filter are counted name : string or None; optional For '.xlsx' files, this string is used for sheet naming. If None and writing an '.xlsx' file, a ValueError is raised. Notes ----- The output files contain the maximums, minimums, abs-max tables. The extrema value is also included along with the case that produced it. After those three tables, a table of bin counts is printed showing the number of extrema values produced by each event. """ if len(res.drminfo.labels) != res.mx.shape[0]: raise ValueError("length of `labels` does not match number of" f" rows in `res.mx` (`desc` = {res.drminfo.desc})") rows = np.arange(res.mx.shape[0]) + 1 headers = ["Row", "Description", *res.cases, "Maximum", "Case"] header = title + "\n\n" + _get_rpt_headers(res) + "\n" amx, amxcase, aext = _get_absmax(res) # order of tables: max, min, abs-max with sign: loop_vars = ( ("Maximum", res.mx, np.nanargmax(res.mx, axis=1), res.ext[:, 0], res.maxcase), ("Minimum", res.mn, np.nanargmin(res.mn, axis=1), res.ext[:, 1], res.mincase), ("Abs-Max", amx, np.nanargmax(abs(amx), axis=1), aext, amxcase), ) if isinstance(filename, xlsxwriter.workbook.Workbook): excel = "old" elif isinstance(filename, str) and filename.endswith(".xlsx"): excel = "new" else: excel = "" if not excel: desclen = max(15, len(max(res.drminfo.labels, key=len))) caselen = max(13, len(max(res.cases, key=len))) n = len(res.cases) widths = [6, desclen, *([caselen] * n), caselen, caselen] descfrm = f"{{:{desclen:d}}}" numform = f"{{:{caselen}.6e}}" formats = ["{:6d}", descfrm, *([numform] * n), numform, "{}"] hu, frm = writer.formheader(headers, widths, formats, sep=[0, 1], just="c", ulchar="=") _wttab( filename, header, hu, frm, res, loop_vars, rows, count_filter, desclen, descfrm, caselen, ) else: if not name: raise ValueError('`name` must be input when writing ".xlsx" files') if excel == "new": opts = {"nan_inf_to_errors": True} with xlsxwriter.Workbook(filename, opts) as workbook: _wtxlsx(workbook, header, headers, res, loop_vars, name, rows, count_filter) else: _wtxlsx(filename, header, headers, res, loop_vars, name, rows, count_filter)
if doabsmax and not one_col: mx = nan_absmax(res.ext[:, 0], res.ext[:, 1])[0] else: mx = res.ext if one_col else res.ext[:, col] _add_column(hdr, numform, mx, w, 4, "c", print_info) # x if res.ext_x is not None: t = res.ext_x if one_col else res.ext_x[:, col] _add_column(domain, "{:8.3f}", t, 8, 2, "c", print_info) # case if case is not None: _add_column("Case", casefrm, case, casewidth, 2, "l", print_info) if doabsmax or one_col: break hu, frm = writer.formheader( print_info.headers, print_info.widths, print_info.formats, print_info.seps, print_info.justs, ) # format page header: header = title + "\n\n" + _get_rpt_headers(res) + "\n" + hu _wtext(filename, header, frm, print_info.printargs, perpage, nrows)
def test_formheader(): descs = ["Item 1", "A different item"] mx = np.array([[1.2, 2.3], [3.4, 4.5]]) * 1000 time = np.array([[1.234], [2.345]]) formats = ["{:<25s}", "{:10.2f}", "{:8.3f}"] widths = [25, 10, 8] assert_raises(ValueError, writer.formheader, 44, widths, formats, sep=[4, 5, 2], just=0) headers = [["The"] * 3, ["Descriptions", "Maximum", "Time", "BAD"]] assert_raises(ValueError, writer.formheader, headers, widths, formats, sep=[4, 5, 2], just=0) headers = [["The"] * 3, ["Descriptions", "Maximum", "Time"]] assert_raises(ValueError, writer.formheader, headers, [25, 10], formats, sep=[4, 5, 2], just=0) hu, f = writer.formheader(headers, widths, formats, sep=[4, 5, 2], just=0) with StringIO() as fout: fout.write(hu) writer.vecwrite(fout, f, descs, mx, time) s = fout.getvalue() sbe = (" The The The\n" " Descriptions Maximum Time\n" " ------------------------- ---------- --------\n" " Item 1 1200.00 2300.000\n" " A different item 3400.00 4500.000\n") assert sbe == s headers = ["Descriptions", "Maximum", "Time"] hu, f = writer.formheader(headers, widths, formats, sep=[4, 5, 2], just=0) with StringIO() as fout: fout.write(hu) writer.vecwrite(fout, f, descs, mx, time) s = fout.getvalue() sbe = (" Descriptions Maximum Time\n" " ------------------------- ---------- --------\n" " Item 1 1200.00 2300.000\n" " A different item 3400.00 4500.000\n") assert sbe == s headers = ["Descriptions", "Maximum", "Time"] hu, f = writer.formheader(headers, widths, formats, sep=2, just=("l", "c", "r")) with StringIO() as fout: fout.write(hu) writer.vecwrite(fout, f, descs, mx, time) s = fout.getvalue() sbe = (" Descriptions Maximum Time\n" " ------------------------- ---------- --------\n" " Item 1 1200.00 2300.000\n" " A different item 3400.00 4500.000\n") assert sbe == s