diff --git a/pyrevitlib/pyrevit/output/__init__.py b/pyrevitlib/pyrevit/output/__init__.py index a3ebcd51b..7e5f817eb 100644 --- a/pyrevitlib/pyrevit/output/__init__.py +++ b/pyrevitlib/pyrevit/output/__init__.py @@ -39,15 +39,12 @@ from pyrevit.userconfig import user_config from pyrevit import DB - -#pylint: disable=W0703,C0302,C0103 mlogger = logger.get_logger(__name__) - DEFAULT_STYLESHEET_NAME = 'outputstyles.css' -def docclosing_eventhandler(sender, args): #pylint: disable=W0613 +def docclosing_eventhandler(sender, args): """Close all output window on document closing.""" ScriptConsoleManager.CloseActiveOutputWindows() @@ -97,7 +94,10 @@ def reset_stylesheet(): class PyRevitOutputWindow(object): """Wrapper to interact with the output window.""" - + + def __init__(self): + self._table_counter = 0 + @property def window(self): """``PyRevitLabs.PyRevit.Runtime.ScriptConsole``: Return output window object.""" @@ -551,16 +551,170 @@ def print_md(md_str): if PY3: sys.stdout.flush() - def print_table(self, table_data, columns=None, formats=None, - title='', last_line_style=''): - """Print provided data in a table in output window. + + def table_html_header(self, columns, table_uid, border_style): + """Helper method for print_table() method + + Return html row for the table header + + Args: + columns (list[str]): list of column names + table_uid (str): a unique ID for this table's CSS classes + border_style (str): CSS border style string for table cells + + Returns: + str: HTML string containing the table header row + + Examples: + ```python + output = pyrevit.output.get_output() + + # Basic usage - called internally by print_table() + columns = ["Name", "Age", "City"] + table_uid = 1 + border_style = "border: 1px solid black;" + header_html = output.table_html_header( + columns, table_uid, border_style) + # Returns: "" \ + # "Name" \ + # "Age" \ + # "City" \ + # "" + + # Without border style + header_html = output.table_html_header( + columns, table_uid, "") + # Returns: "" \ + # "Name" \ + # "Age" \ + # "City" \ + # "" + ``` + """ + html_head = "".format(border_style) + for i, c in enumerate(columns): + html_head += \ + "{}".format( + table_uid, i, c) + # pyRevit original print_table uses align='left'. + # This is now overridden by CSS if specified + html_head += "" + + return html_head + + + def table_check_input_lists(self, + table_data, + columns, + formats, + input_kwargs): + """Helper method for print_table() method + + Check that the table_data is present and is a list + Check that table_data rows are of the same length + Check that all print_table() kwargs of type list are of correct length + + Args: + table_data (list[list[Any]]): The whole table data as 2D list + columns (list[str]): list of column names + formats (list[str]): list of format strings for each column + input_kwargs (list[list[Any]]): list of additional argument lists + + Returns: + tuple: (bool, str) - (True/False, message) indicating result + + Examples: + ```python + output = pyrevit.output.get_output() + + # Valid table data + table_data = [["John", 25, "NYC"], ["Jane", 30, "LA"]] + columns = ["Name", "Age", "City"] + formats = ["", "{} years", ""] + input_kwargs = [["left", "center", "right"], + ["100px", "80px", "120px"]] + + is_valid, message = output.table_check_input_lists( + table_data, columns, formats, input_kwargs) + # Returns: (True, "Inputs OK") + + # Invalid - mismatched column count + table_data = [["John", 25], ["Jane", 30, "LA"]] # Inconsistent + is_valid, message = output.table_check_input_lists( + table_data, columns, formats, input_kwargs) + # Returns: (False, "Not all rows of table_data are of " + # "equal length") + + # Invalid - wrong number of columns + columns = ["Name", "Age"] # Only 2 columns but data has 3 + is_valid, message = output.table_check_input_lists( + table_data, columns, formats, input_kwargs) + # Returns: (False, "Column head list length not equal " + # "to data row") + + # Invalid - empty table data + is_valid, message = output.table_check_input_lists( + [], columns, formats, input_kwargs) + # Returns: (False, "No table_data list") + ``` + """ + + # First check positional and named keyword args + if not table_data: + return False, "No table_data list" + if not isinstance(table_data, list): + return False, "table_data is not a list" + # table_data is a list. The first sublist must also be a list + first_data_row = list(table_data[0]) + if not isinstance(first_data_row, list): + return False, "table_data's first row is not a list or tuple ({})".format(type(first_data_row)) + len_data_row = len(first_data_row) + if not all(len(row) == len_data_row for row in table_data): + return False, "Not all rows of table_data are of equal length" + + if columns and len_data_row != len(columns): # columns is allowed to be None + return False, "Column head list length not equal to data row" + + if formats and len_data_row != len(formats): # formats is allowed to be None + return False, "Formats list length not equal to data row" + + # Next check **kwargs + # Loop through the lists and return if not a list or len not equal + for kwarg_list in input_kwargs: + if not kwarg_list: # No kwarg is OK beacause they are optional + continue + if not isinstance(kwarg_list, list): + return False, "One of the print_table kwargs that should be a list is not a list ({})".format(kwarg_list) + if len(kwarg_list) != len_data_row: + return False, "print_table kwarg list length problem (should match {} columns)".format(len_data_row) + + return True, "Inputs OK" + + + def print_table(self, + table_data, + columns=None, + formats=None, + title='', + last_line_style='', + **kwargs): + """Print provided data in a HTML table in output window. + The same window can output several tables, each with their own formatting options. Args: table_data (list[iterable[Any]]): 2D array of data title (str): table title columns (list[str]): list of column names - formats (list[str]): column data formats - last_line_style (str): css style of last row + formats (list[str]): column data formats using python string formatting + last_line_style (str): css style of last row of data (NB applies to all tables in this output) + column_head_align_styles (list[str]): list css align-text styles for header row + column_data_align_styles (list[str]): list css align-text styles for data rows + column_widths (list[str]): list of CSS widths in either px or % + column_vertical_border_style (str): CSS compact border definition + table_width_style (str): CSS to use for width for the whole table, in either px or % + repeat_head_as_foot (bool): Repeat the header row at the table foot (useful for long tables) + row_striping (bool): False to override the default white-grey row stripes and make all white) + Examples: ```python @@ -573,56 +727,125 @@ def print_table(self, table_data, columns=None, formats=None, title="Example Table", columns=["Row Name", "Column 1", "Column 2", "Percentage"], formats=['', '', '', '{}%'], - last_line_style='color:red;' + last_line_style='color:red;', + col_head_align_styles = ["left", "left", "center", "right"], + col_data_align_styles = ["left", "left", "center", "right"], + column_widths = ["100px", "100px", "500px", "100px"], + column_vertical_border_style = "border:black solid 1px", + table_width_style='width:100%', + repeat_head_as_foot=True, + row_striping=False + ) + Returns: + Directly prints: + print_md of the title (str): + print_html of the generated HTML table with formatting. ``` """ - if not columns: - columns = [] - if not formats: - formats = [] - if last_line_style: - self.add_style('tr:last-child {{ {style} }}' - .format(style=last_line_style)) + # Get user formatting options from kwargs + column_head_align_styles = kwargs.get("column_head_align_styles", None) + column_data_align_styles = kwargs.get("column_data_align_styles", None) + column_widths = kwargs.get("column_widths", None) + column_vertical_border_style = kwargs.get("column_vertical_border_style", None) + table_width_style = kwargs.get("table_width_style", None) + repeat_head_as_foot = kwargs.get("repeat_head_as_foot", False) + row_striping = kwargs.get("row_striping", True) + + + # Get a unique ID for each table from _table_counter + # This is used in HTML tags to define CSS classes for formatting per table + self._table_counter += 1 + table_uid = self._table_counter + + # Validate input arguments should be lists: + input_kwargs = [column_head_align_styles, column_data_align_styles, column_widths] + inputs_ok, inputs_msg = self.table_check_input_lists(table_data, + columns, + formats, + input_kwargs) + + if not inputs_ok: + self.print_md('### :warning: {} '.format(inputs_msg)) + return + + + if not row_striping: + # Override default original pyRevit white-grey row striping. Makes all rows white. + self.add_style('tr.data-row-{} {{ {style} }}'.format(table_uid, style="background-color: #ffffff")) - adjust_base_col = '|' - adjust_extra_col = ':---|' - base_col = '|' - extra_col = '{data}|' - - # find max column count - max_col = max([len(x) for x in table_data]) + if last_line_style: + # Adds a CCS class to allow a last-row format per table (if several in the same output) + self.add_style('tr.data-row-{}:last-child {{ {style} }}'.format(table_uid, style=last_line_style)) + + if column_head_align_styles: + for i, s in enumerate(column_head_align_styles): + self.add_style('.head_title-{}-{} {{ text-align:{style} }}'.format(table_uid, i, style=s)) + + if column_data_align_styles: + for i, s in enumerate(column_data_align_styles): + self.add_style('.data_cell-{}-{} {{ text-align:{style} }}'.format(table_uid, i, style=s)) + + if table_width_style: + self.add_style('.tab-{} {{ width:{} }}'.format(table_uid, table_width_style)) + + + # Open HTML table and its CSS class + html = "".format(table_uid) + + # Build colgroup if user argument specifies column widths + if column_widths: + COL = "" + html += '' + for w in column_widths: + html += COL.format(w) + html += "" + + if column_vertical_border_style: + border_style = "style='{}'".format(column_vertical_border_style) + else: + border_style = "" - header = '' + # Build header row (column titles) if requested if columns: - header = base_col - for idx, col_name in zip_longest(range(max_col), columns, fillvalue=''): #pylint: disable=W0612 - header += extra_col.format(data=col_name) - - header += '\n' - - justifier = adjust_base_col - for idx in range(max_col): - justifier += adjust_extra_col - - justifier += '\n' - - rows = '' - for entry in table_data: - row = base_col - for idx, attrib, attr_format \ - in zip_longest(range(max_col), entry, formats, fillvalue=''): - if attr_format: - value = attr_format.format(attrib) - else: - value = attrib - row += extra_col.format(data=value) - rows += row + '\n' - - table = header + justifier + rows - self.print_md('### {title}'.format(title=title)) - self.print_md(table) + html_head = self.table_html_header(columns, table_uid, border_style) + html += html_head + else: + html_head ="" + repeat_head_as_foot = False + + + # Build body rows from 2D python list table_data + html += "" + for row in table_data: + # Build an HTML data row with CSS class for this table + html += "".format(table_uid) + for i, cell_data in enumerate(row): + + # Slight workaround to be backwards compatible with pyRevit original print_table + # pyRevit documentation gives the example: formats=['', '', '', '{}%'], + # Sometimes giving an empty string, sometimes a placeholder with string formatting + if formats: # If format options provided + format_placeholder = formats[i] if formats[i] else "{}" + + cell_data = format_placeholder.format(cell_data) # Insert data with or without formatting + + html += "".format(table_uid, i, border_style, cell_data) + html += "" + + # Re-insert header row at the end, if requested and if column titles provided + if repeat_head_as_foot: + html += html_head + + + html += "" + html += "
{}
" # Close table + + if title: + self.print_md('### {title}'.format(title=title)) + self.print_html(html) + def print_image(self, image_path): r"""Prints given image to the output.