Skip to content

Commit 0fb8c5e

Browse files
authored
Merge pull request #2827 from nasmovk/patch-2
Rewrite of the print_table function: module "output" __init__.py
2 parents f0c3530 + 2b0079a commit 0fb8c5e

File tree

1 file changed

+276
-53
lines changed

1 file changed

+276
-53
lines changed

pyrevitlib/pyrevit/output/__init__.py

Lines changed: 276 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,12 @@
3939
from pyrevit.userconfig import user_config
4040
from pyrevit import DB
4141

42-
43-
#pylint: disable=W0703,C0302,C0103
4442
mlogger = logger.get_logger(__name__)
4543

46-
4744
DEFAULT_STYLESHEET_NAME = 'outputstyles.css'
4845

4946

50-
def docclosing_eventhandler(sender, args): #pylint: disable=W0613
47+
def docclosing_eventhandler(sender, args):
5148
"""Close all output window on document closing."""
5249
ScriptConsoleManager.CloseActiveOutputWindows()
5350

@@ -97,7 +94,10 @@ def reset_stylesheet():
9794

9895
class PyRevitOutputWindow(object):
9996
"""Wrapper to interact with the output window."""
100-
97+
98+
def __init__(self):
99+
self._table_counter = 0
100+
101101
@property
102102
def window(self):
103103
"""``PyRevitLabs.PyRevit.Runtime.ScriptConsole``: Return output window object."""
@@ -551,16 +551,170 @@ def print_md(md_str):
551551
if PY3:
552552
sys.stdout.flush()
553553

554-
def print_table(self, table_data, columns=None, formats=None,
555-
title='', last_line_style=''):
556-
"""Print provided data in a table in output window.
554+
555+
def table_html_header(self, columns, table_uid, border_style):
556+
"""Helper method for print_table() method
557+
558+
Return html <thead><tr><th> row for the table header
559+
560+
Args:
561+
columns (list[str]): list of column names
562+
table_uid (str): a unique ID for this table's CSS classes
563+
border_style (str): CSS border style string for table cells
564+
565+
Returns:
566+
str: HTML string containing the table header row
567+
568+
Examples:
569+
```python
570+
output = pyrevit.output.get_output()
571+
572+
# Basic usage - called internally by print_table()
573+
columns = ["Name", "Age", "City"]
574+
table_uid = 1
575+
border_style = "border: 1px solid black;"
576+
header_html = output.table_html_header(
577+
columns, table_uid, border_style)
578+
# Returns: "<thead><tr style='border: 1px solid black;'>" \
579+
# "<th class='head_title-1-0' align='left'>Name</th>" \
580+
# "<th class='head_title-1-1' align='left'>Age</th>" \
581+
# "<th class='head_title-1-2' align='left'>City</th>" \
582+
# "</tr></thead>"
583+
584+
# Without border style
585+
header_html = output.table_html_header(
586+
columns, table_uid, "")
587+
# Returns: "<thead><tr>" \
588+
# "<th class='head_title-1-0' align='left'>Name</th>" \
589+
# "<th class='head_title-1-1' align='left'>Age</th>" \
590+
# "<th class='head_title-1-2' align='left'>City</th>" \
591+
# "</tr></thead>"
592+
```
593+
"""
594+
html_head = "<thead><tr {}>".format(border_style)
595+
for i, c in enumerate(columns):
596+
html_head += \
597+
"<th class='head_title-{}-{}' align='left'>{}</th>".format(
598+
table_uid, i, c)
599+
# pyRevit original print_table uses align='left'.
600+
# This is now overridden by CSS if specified
601+
html_head += "</tr></thead>"
602+
603+
return html_head
604+
605+
606+
def table_check_input_lists(self,
607+
table_data,
608+
columns,
609+
formats,
610+
input_kwargs):
611+
"""Helper method for print_table() method
612+
613+
Check that the table_data is present and is a list
614+
Check that table_data rows are of the same length
615+
Check that all print_table() kwargs of type list are of correct length
616+
617+
Args:
618+
table_data (list[list[Any]]): The whole table data as 2D list
619+
columns (list[str]): list of column names
620+
formats (list[str]): list of format strings for each column
621+
input_kwargs (list[list[Any]]): list of additional argument lists
622+
623+
Returns:
624+
tuple: (bool, str) - (True/False, message) indicating result
625+
626+
Examples:
627+
```python
628+
output = pyrevit.output.get_output()
629+
630+
# Valid table data
631+
table_data = [["John", 25, "NYC"], ["Jane", 30, "LA"]]
632+
columns = ["Name", "Age", "City"]
633+
formats = ["", "{} years", ""]
634+
input_kwargs = [["left", "center", "right"],
635+
["100px", "80px", "120px"]]
636+
637+
is_valid, message = output.table_check_input_lists(
638+
table_data, columns, formats, input_kwargs)
639+
# Returns: (True, "Inputs OK")
640+
641+
# Invalid - mismatched column count
642+
table_data = [["John", 25], ["Jane", 30, "LA"]] # Inconsistent
643+
is_valid, message = output.table_check_input_lists(
644+
table_data, columns, formats, input_kwargs)
645+
# Returns: (False, "Not all rows of table_data are of "
646+
# "equal length")
647+
648+
# Invalid - wrong number of columns
649+
columns = ["Name", "Age"] # Only 2 columns but data has 3
650+
is_valid, message = output.table_check_input_lists(
651+
table_data, columns, formats, input_kwargs)
652+
# Returns: (False, "Column head list length not equal "
653+
# "to data row")
654+
655+
# Invalid - empty table data
656+
is_valid, message = output.table_check_input_lists(
657+
[], columns, formats, input_kwargs)
658+
# Returns: (False, "No table_data list")
659+
```
660+
"""
661+
662+
# First check positional and named keyword args
663+
if not table_data:
664+
return False, "No table_data list"
665+
if not isinstance(table_data, list):
666+
return False, "table_data is not a list"
667+
# table_data is a list. The first sublist must also be a list
668+
first_data_row = list(table_data[0])
669+
if not isinstance(first_data_row, list):
670+
return False, "table_data's first row is not a list or tuple ({})".format(type(first_data_row))
671+
len_data_row = len(first_data_row)
672+
if not all(len(row) == len_data_row for row in table_data):
673+
return False, "Not all rows of table_data are of equal length"
674+
675+
if columns and len_data_row != len(columns): # columns is allowed to be None
676+
return False, "Column head list length not equal to data row"
677+
678+
if formats and len_data_row != len(formats): # formats is allowed to be None
679+
return False, "Formats list length not equal to data row"
680+
681+
# Next check **kwargs
682+
# Loop through the lists and return if not a list or len not equal
683+
for kwarg_list in input_kwargs:
684+
if not kwarg_list: # No kwarg is OK beacause they are optional
685+
continue
686+
if not isinstance(kwarg_list, list):
687+
return False, "One of the print_table kwargs that should be a list is not a list ({})".format(kwarg_list)
688+
if len(kwarg_list) != len_data_row:
689+
return False, "print_table kwarg list length problem (should match {} columns)".format(len_data_row)
690+
691+
return True, "Inputs OK"
692+
693+
694+
def print_table(self,
695+
table_data,
696+
columns=None,
697+
formats=None,
698+
title='',
699+
last_line_style='',
700+
**kwargs):
701+
"""Print provided data in a HTML table in output window.
702+
The same window can output several tables, each with their own formatting options.
557703
558704
Args:
559705
table_data (list[iterable[Any]]): 2D array of data
560706
title (str): table title
561707
columns (list[str]): list of column names
562-
formats (list[str]): column data formats
563-
last_line_style (str): css style of last row
708+
formats (list[str]): column data formats using python string formatting
709+
last_line_style (str): css style of last row of data (NB applies to all tables in this output)
710+
column_head_align_styles (list[str]): list css align-text styles for header row
711+
column_data_align_styles (list[str]): list css align-text styles for data rows
712+
column_widths (list[str]): list of CSS widths in either px or %
713+
column_vertical_border_style (str): CSS compact border definition
714+
table_width_style (str): CSS to use for width for the whole table, in either px or %
715+
repeat_head_as_foot (bool): Repeat the header row at the table foot (useful for long tables)
716+
row_striping (bool): False to override the default white-grey row stripes and make all white)
717+
564718
565719
Examples:
566720
```python
@@ -573,56 +727,125 @@ def print_table(self, table_data, columns=None, formats=None,
573727
title="Example Table",
574728
columns=["Row Name", "Column 1", "Column 2", "Percentage"],
575729
formats=['', '', '', '{}%'],
576-
last_line_style='color:red;'
730+
last_line_style='color:red;',
731+
col_head_align_styles = ["left", "left", "center", "right"],
732+
col_data_align_styles = ["left", "left", "center", "right"],
733+
column_widths = ["100px", "100px", "500px", "100px"],
734+
column_vertical_border_style = "border:black solid 1px",
735+
table_width_style='width:100%',
736+
repeat_head_as_foot=True,
737+
row_striping=False
738+
577739
)
740+
Returns:
741+
Directly prints:
742+
print_md of the title (str):
743+
print_html of the generated HTML table with formatting.
578744
```
579745
"""
580-
if not columns:
581-
columns = []
582-
if not formats:
583-
formats = []
584746

585-
if last_line_style:
586-
self.add_style('tr:last-child {{ {style} }}'
587-
.format(style=last_line_style))
747+
# Get user formatting options from kwargs
748+
column_head_align_styles = kwargs.get("column_head_align_styles", None)
749+
column_data_align_styles = kwargs.get("column_data_align_styles", None)
750+
column_widths = kwargs.get("column_widths", None)
751+
column_vertical_border_style = kwargs.get("column_vertical_border_style", None)
752+
table_width_style = kwargs.get("table_width_style", None)
753+
repeat_head_as_foot = kwargs.get("repeat_head_as_foot", False)
754+
row_striping = kwargs.get("row_striping", True)
755+
756+
757+
# Get a unique ID for each table from _table_counter
758+
# This is used in HTML tags to define CSS classes for formatting per table
759+
self._table_counter += 1
760+
table_uid = self._table_counter
761+
762+
# Validate input arguments should be lists:
763+
input_kwargs = [column_head_align_styles, column_data_align_styles, column_widths]
764+
inputs_ok, inputs_msg = self.table_check_input_lists(table_data,
765+
columns,
766+
formats,
767+
input_kwargs)
768+
769+
if not inputs_ok:
770+
self.print_md('### :warning: {} '.format(inputs_msg))
771+
return
772+
773+
774+
if not row_striping:
775+
# Override default original pyRevit white-grey row striping. Makes all rows white.
776+
self.add_style('tr.data-row-{} {{ {style} }}'.format(table_uid, style="background-color: #ffffff"))
588777

589-
adjust_base_col = '|'
590-
adjust_extra_col = ':---|'
591-
base_col = '|'
592-
extra_col = '{data}|'
593-
594-
# find max column count
595-
max_col = max([len(x) for x in table_data])
778+
if last_line_style:
779+
# Adds a CCS class to allow a last-row format per table (if several in the same output)
780+
self.add_style('tr.data-row-{}:last-child {{ {style} }}'.format(table_uid, style=last_line_style))
781+
782+
if column_head_align_styles:
783+
for i, s in enumerate(column_head_align_styles):
784+
self.add_style('.head_title-{}-{} {{ text-align:{style} }}'.format(table_uid, i, style=s))
785+
786+
if column_data_align_styles:
787+
for i, s in enumerate(column_data_align_styles):
788+
self.add_style('.data_cell-{}-{} {{ text-align:{style} }}'.format(table_uid, i, style=s))
789+
790+
if table_width_style:
791+
self.add_style('.tab-{} {{ width:{} }}'.format(table_uid, table_width_style))
792+
793+
794+
# Open HTML table and its CSS class
795+
html = "<table class='tab-{}'>".format(table_uid)
796+
797+
# Build colgroup if user argument specifies column widths
798+
if column_widths:
799+
COL = "<col style='width: {}'>"
800+
html += '<colgroup>'
801+
for w in column_widths:
802+
html += COL.format(w)
803+
html += "</colgroup>"
804+
805+
if column_vertical_border_style:
806+
border_style = "style='{}'".format(column_vertical_border_style)
807+
else:
808+
border_style = ""
596809

597-
header = ''
810+
# Build header row (column titles) if requested
598811
if columns:
599-
header = base_col
600-
for idx, col_name in zip_longest(range(max_col), columns, fillvalue=''): #pylint: disable=W0612
601-
header += extra_col.format(data=col_name)
602-
603-
header += '\n'
604-
605-
justifier = adjust_base_col
606-
for idx in range(max_col):
607-
justifier += adjust_extra_col
608-
609-
justifier += '\n'
610-
611-
rows = ''
612-
for entry in table_data:
613-
row = base_col
614-
for idx, attrib, attr_format \
615-
in zip_longest(range(max_col), entry, formats, fillvalue=''):
616-
if attr_format:
617-
value = attr_format.format(attrib)
618-
else:
619-
value = attrib
620-
row += extra_col.format(data=value)
621-
rows += row + '\n'
622-
623-
table = header + justifier + rows
624-
self.print_md('### {title}'.format(title=title))
625-
self.print_md(table)
812+
html_head = self.table_html_header(columns, table_uid, border_style)
813+
html += html_head
814+
else:
815+
html_head =""
816+
repeat_head_as_foot = False
817+
818+
819+
# Build body rows from 2D python list table_data
820+
html += "<tbody>"
821+
for row in table_data:
822+
# Build an HTML data row with CSS class for this table
823+
html += "<tr class='data-row-{}'>".format(table_uid)
824+
for i, cell_data in enumerate(row):
825+
826+
# Slight workaround to be backwards compatible with pyRevit original print_table
827+
# pyRevit documentation gives the example: formats=['', '', '', '{}%'],
828+
# Sometimes giving an empty string, sometimes a placeholder with string formatting
829+
if formats: # If format options provided
830+
format_placeholder = formats[i] if formats[i] else "{}"
831+
832+
cell_data = format_placeholder.format(cell_data) # Insert data with or without formatting
833+
834+
html += "<td class='data_cell-{}-{}' {}>{}</td>".format(table_uid, i, border_style, cell_data)
835+
html += "</tr>"
836+
837+
# Re-insert header row at the end, if requested and if column titles provided
838+
if repeat_head_as_foot:
839+
html += html_head
840+
841+
842+
html += "</tbody>"
843+
html += "</table>" # Close table
844+
845+
if title:
846+
self.print_md('### {title}'.format(title=title))
847+
self.print_html(html)
848+
626849

627850
def print_image(self, image_path):
628851
r"""Prints given image to the output.

0 commit comments

Comments
 (0)