Skip to content

Commit 173de85

Browse files
authored
Get rid of pandas dependency (#222)
Use an HTML template to represent the tabular data.
1 parent b1d4f25 commit 173de85

3 files changed

Lines changed: 130 additions & 26 deletions

File tree

home/process.py

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import inspect
66
import os
7+
import re
78
import sys
89
import threading
910
import time
@@ -13,7 +14,6 @@
1314
from collections.abc import Mapping
1415

1516
import ipywidgets as ipw
16-
import pandas as pd
1717
import traitlets as tl
1818

1919
# AiiDA imports
@@ -27,6 +27,7 @@
2727
from aiida.common.links import LinkType
2828
from aiida.tools.query.calculation import CalculationQueryBuilder
2929
from IPython.display import HTML, Javascript, clear_output, display
30+
from jinja2 import Template
3031

3132

3233
class CantRegisterCallbackError(Exception):
@@ -38,6 +39,85 @@ def __init__(self, function):
3839
)
3940

4041

42+
PROCESS_TABLE_TEMPLATE = Template(
43+
"""
44+
<style>
45+
.df { border: none; }
46+
.df tbody tr:nth-child(odd) { background-color: #e5e7e9; }
47+
.df tbody tr:nth-child(odd):hover { background-color: #f5b7b1; }
48+
.df tbody tr:nth-child(even):hover { background-color: #f5b7b1; }
49+
.df tbody td { min-width: 150px; text-align: center; border: none; }
50+
.df th { text-align: center; border: none; border-bottom: 1px solid black; }
51+
</style>
52+
<table class="df">
53+
<thead>
54+
<tr>
55+
{% for header in headers %}
56+
<th>{{ header }}</th>
57+
{% endfor %}
58+
</tr>
59+
</thead>
60+
<tbody>
61+
{% for row in rows %}
62+
<tr>
63+
{% for header in headers %}
64+
<td>{{ row[header] }}</td>
65+
{% endfor %}
66+
</tr>
67+
{% endfor %}
68+
</tbody>
69+
</table>
70+
"""
71+
)
72+
73+
74+
def _stringify_process_cell(value):
75+
if value is None:
76+
return ""
77+
return str(value)
78+
79+
80+
def _normalize_process_rows(projected):
81+
if not projected:
82+
return [], []
83+
84+
headers = list(projected[0])
85+
rows = [
86+
{
87+
header: _stringify_process_cell(value)
88+
for header, value in zip(headers, entry)
89+
}
90+
for entry in projected[1:]
91+
]
92+
return headers, rows
93+
94+
95+
def _filter_process_rows(rows, description_contains):
96+
if not description_contains:
97+
return rows
98+
99+
pattern = re.compile(description_contains)
100+
return [row for row in rows if pattern.search(row.get("Description", ""))]
101+
102+
103+
def _add_process_links(rows, path_to_root):
104+
linked_rows = []
105+
for row in rows:
106+
linked_row = dict(row)
107+
pk = linked_row.get("PK", "")
108+
linked_row["PK"] = (
109+
f"""<a href={path_to_root}home/process.ipynb?id={pk} target="_blank">{pk}</a>"""
110+
if pk
111+
else ""
112+
)
113+
linked_rows.append(linked_row)
114+
return linked_rows
115+
116+
117+
def _render_process_table(headers, rows):
118+
return PROCESS_TABLE_TEMPLATE.render(headers=headers, rows=rows)
119+
120+
41121
def get_running_calcs(process):
42122
"""Takes a process and yeilds running children calculations."""
43123

@@ -576,22 +656,6 @@ def __init__(self, path_to_root="../", **kwargs):
576656

577657
def update(self, _=None):
578658
"""Perform the query."""
579-
pd.set_option("max_colwidth", 40)
580-
# Here we are defining properties of 'df' class (specified while exporting pandas table into html).
581-
# Since the exported object is nothing more than HTML table, all 'standard' HTML table settings
582-
# can be applied to it as well.
583-
# For more information on how to controle the table appearance please visit:
584-
# https://css-tricks.com/complete-guide-table-element/
585-
self.table.value = """
586-
<style>
587-
.df { border: none; }
588-
.df tbody tr:nth-child(odd) { background-color: #e5e7e9; }
589-
.df tbody tr:nth-child(odd):hover { background-color: #f5b7b1; }
590-
.df tbody tr:nth-child(even):hover { background-color: #f5b7b1; }
591-
.df tbody td { min-width: 150px; text-align: center; border: none }
592-
.df th { text-align: center; border: none; border-bottom: 1px solid black;}
593-
</style>
594-
"""
595659
builder = CalculationQueryBuilder()
596660
filters = builder.get_filters(
597661
all_entries=False,
@@ -630,19 +694,16 @@ def update(self, _=None):
630694
"description",
631695
],
632696
)
633-
dataf = pd.DataFrame(projected[1:], columns=projected[0])
697+
headers, rows = _normalize_process_rows(projected)
634698

635699
# Keep only process that contain the requested string in the description.
636-
if self.description_contains:
637-
dataf = dataf[dataf.Description.str.contains(self.description_contains)]
700+
rows = _filter_process_rows(rows, self.description_contains)
638701

639-
self.output.value = f"{len(dataf)} processes shown"
702+
self.output.value = f"{len(rows)} processes shown"
640703

641704
# Add HTML links.
642-
dataf["PK"] = dataf["PK"].apply(
643-
lambda x: f"""<a href={self.path_to_root}home/process.ipynb?id={x} target="_blank">{x}</a>"""
644-
)
645-
self.table.value += dataf.to_html(classes="df", escape=False, index=False)
705+
rows = _add_process_links(rows, self.path_to_root)
706+
self.table.value = _render_process_table(headers, rows)
646707

647708
@tl.validate("incoming_node")
648709
def _validate_incoming_node(self, provided):

setup.cfg

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ install_requires =
2828
humanfriendly~=10.0
2929
ipython~=7.0
3030
ipywidgets~=8.0
31-
pandas~=2.2
3231
traitlets~=5.0
3332
nbclassic~=1.3
3433
pexpect~=4.9

tests/test_process.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,47 @@ def test_process_list_widget(multiply_add_completed_workchain):
104104
widget.update()
105105
assert "processes shown" in widget.output.value
106106
assert "<table" in widget.table.value
107+
assert (
108+
f"home/process.ipynb?id={multiply_add_completed_workchain.pk}"
109+
in widget.table.value
110+
)
111+
112+
113+
def test_process_list_widget_filters_descriptions(generate_calc_job_node):
114+
matching_process = generate_calc_job_node(inputs={"parameters": orm.Int(1)})
115+
matching_process.description = "calc-42 complete"
116+
117+
other_process = generate_calc_job_node(inputs={"parameters": orm.Int(2)})
118+
other_process.description = "skip me"
119+
120+
process_without_description = generate_calc_job_node(
121+
inputs={"parameters": orm.Int(3)}
122+
)
123+
124+
widget = home_process.ProcessListWidget()
125+
widget.description_contains = r"calc-\d+"
126+
widget.update()
127+
128+
assert widget.output.value == "1 processes shown"
129+
assert "calc-42 complete" in widget.table.value
130+
assert "skip me" not in widget.table.value
131+
assert f"home/process.ipynb?id={matching_process.pk}" in widget.table.value
132+
assert f"home/process.ipynb?id={other_process.pk}" not in widget.table.value
133+
assert (
134+
f"home/process.ipynb?id={process_without_description.pk}"
135+
not in widget.table.value
136+
)
137+
138+
139+
def test_process_list_widget_renders_empty_results(multiply_add_completed_workchain):
140+
widget = home_process.ProcessListWidget()
141+
widget.process_label = "definitely-no-such-process-label"
142+
widget.update()
143+
144+
assert widget.output.value == "0 processes shown"
145+
assert "<table" in widget.table.value
146+
assert "<th>PK</th>" in widget.table.value
147+
assert (
148+
f"home/process.ipynb?id={multiply_add_completed_workchain.pk}"
149+
not in widget.table.value
150+
)

0 commit comments

Comments
 (0)