Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 276 additions & 53 deletions pyrevitlib/pyrevit/output/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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 <thead><tr><th> 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: "<thead><tr style='border: 1px solid black;'>" \
# "<th class='head_title-1-0' align='left'>Name</th>" \
# "<th class='head_title-1-1' align='left'>Age</th>" \
# "<th class='head_title-1-2' align='left'>City</th>" \
# "</tr></thead>"

# Without border style
header_html = output.table_html_header(
columns, table_uid, "")
# Returns: "<thead><tr>" \
# "<th class='head_title-1-0' align='left'>Name</th>" \
# "<th class='head_title-1-1' align='left'>Age</th>" \
# "<th class='head_title-1-2' align='left'>City</th>" \
# "</tr></thead>"
```
"""
html_head = "<thead><tr {}>".format(border_style)
for i, c in enumerate(columns):
html_head += \
"<th class='head_title-{}-{}' align='left'>{}</th>".format(
table_uid, i, c)
# pyRevit original print_table uses align='left'.
# This is now overridden by CSS if specified
html_head += "</tr></thead>"

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
Expand All @@ -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 = "<table class='tab-{}'>".format(table_uid)

# Build colgroup if user argument specifies column widths
if column_widths:
COL = "<col style='width: {}'>"
html += '<colgroup>'
for w in column_widths:
html += COL.format(w)
html += "</colgroup>"

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 += "<tbody>"
for row in table_data:
# Build an HTML data row with CSS class for this table
html += "<tr class='data-row-{}'>".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 += "<td class='data_cell-{}-{}' {}>{}</td>".format(table_uid, i, border_style, cell_data)
html += "</tr>"

# Re-insert header row at the end, if requested and if column titles provided
if repeat_head_as_foot:
html += html_head


html += "</tbody>"
html += "</table>" # 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.
Expand Down