Skip to content

Commit 79ed29d

Browse files
committed
Refines table rendering and error handling logic
1 parent c391ad1 commit 79ed29d

File tree

7 files changed

+174
-75
lines changed

7 files changed

+174
-75
lines changed

src/easydiffraction/display/tablers/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
class TableBackendBase(ABC):
2121
"""Abstract base class for concrete table backends.
2222
23-
Subclasses implement the ``render`` method which receives an index-aware
24-
pandas DataFrame and the alignment for each column header.
23+
Subclasses implement the ``render`` method which receives an
24+
index-aware pandas DataFrame and the alignment for each column
25+
header.
2526
"""
2627

2728
FLOAT_PRECISION = 5

src/easydiffraction/display/tablers/rich.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(self) -> None:
4141
# Use a wide console to avoid truncation/ellipsis in cells
4242
self.console = Console(
4343
force_jupyter=False,
44-
width=200,
44+
# width=200,
4545
)
4646

4747
@property
@@ -71,15 +71,16 @@ def render(
7171
show_header=True,
7272
header_style='bold',
7373
border_style=color,
74-
expand=True, # to fill all available horizontal space
74+
# expand=True, # to fill all available horizontal space
7575
)
7676

7777
# Add index column header first
7878
table.add_column(justify='right', style=color)
7979

8080
# Add other column headers with alignment
8181
for col, align in zip(df, alignments, strict=False):
82-
table.add_column(str(col), justify=align, no_wrap=True)
82+
# table.add_column(str(col), justify=align, no_wrap=True)
83+
table.add_column(str(col), justify=align, no_wrap=False)
8384

8485
# Add rows (prepend the index value as first column)
8586
for idx, row_values in df.iterrows():

src/easydiffraction/experiments/categories/background/factory.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from typing import TYPE_CHECKING
1414
from typing import Optional
1515

16-
from easydiffraction import log
1716
from easydiffraction.experiments.categories.background.enums import BackgroundTypeEnum
1817

1918
if TYPE_CHECKING:
@@ -58,17 +57,10 @@ def create(
5857
supported = cls._supported_map()
5958
if background_type not in supported:
6059
supported_types = list(supported.keys())
61-
# raise ValueError(
62-
# f"Unsupported background type: '{background_type}'.\n"
63-
# f' Supported background types:
64-
# {[bt.value for bt in supported_types]}'
65-
# )
66-
log.warning(
67-
f"Unknown background type '{background_type}'. "
68-
f'Supported background types: {[bt.value for bt in supported_types]}. '
69-
f"For more information, use 'show_supported_background_types()'"
60+
raise ValueError(
61+
f"Unsupported background type: '{background_type}'. "
62+
f'Supported background types: {[bt.value for bt in supported_types]}'
7063
)
71-
return
7264

7365
background_class = supported[background_type]
7466
return background_class()

src/easydiffraction/io/cif/serialize.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,11 @@ def project_info_to_cif(info) -> str:
150150
last_modified = f"'{info._last_modified.strftime('%d %b %Y %H:%M:%S')}'"
151151

152152
return (
153-
f'_project.id {name}\n'
154-
f'_project.title {title}\n'
155-
f'_project.description {description}\n'
156-
f'_project.created {created}\n'
157-
f'_project.last_modified {last_modified}'
153+
f'_project.id {name}\n'
154+
f'_project.title {title}\n'
155+
f'_project.description {description}\n'
156+
f'_project.created {created}\n'
157+
f'_project.last_modified {last_modified}'
158158
)
159159

160160

src/easydiffraction/summary/summary.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ def show_project_info(self) -> None:
4242

4343
if self.project.info.description:
4444
log.paragraph('Description')
45-
log.print('\n'.join(wrap(self.project.info.description, width=80)))
45+
# log.print('\n'.join(wrap(self.project.info.description, width=80)))
46+
# TODO: Fix the following lines
47+
# Ensure description wraps with explicit newlines for tests
48+
desc_lines = wrap(self.project.info.description, width=60)
49+
# Use plain print to avoid Left padding that would break
50+
# newline adjacency checks
51+
print('\n'.join(desc_lines))
4652

4753
def show_crystallographic_data(self) -> None:
4854
"""Print crystallographic data including phase datablocks, space

src/easydiffraction/utils/env.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def is_colab() -> bool:
2828
return False
2929

3030

31+
# TODO: Consider renaming helpers to is_jupyter or in_jupyter.
3132
def is_notebook() -> bool:
3233
"""Return True when running inside a Jupyter Notebook.
3334

src/easydiffraction/utils/utils.py

Lines changed: 151 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from uncertainties import ufloat
2424
from uncertainties import ufloat_fromstr
2525

26-
import easydiffraction.utils.env as _env
26+
import easydiffraction.utils.env as _env # TODO: Rename to environment?
2727
from easydiffraction import log
2828
from easydiffraction.display.tables import TableRenderer
2929

@@ -276,17 +276,18 @@ def fetch_tutorial_list() -> list[str]:
276276
release_info = _get_release_info(tag)
277277
# Fallback to latest if tag fetch failed and tag was attempted
278278
if release_info is None and tag is not None:
279-
log.error('Falling back to latest release info...')
279+
# Non-fatal during listing; warn and fall back silently
280+
log.warning('Falling back to latest release info...', exc_type=UserWarning)
280281
release_info = _get_release_info(None)
281282
if release_info is None:
282283
return []
283284
tutorial_asset = _get_tutorial_asset(release_info)
284285
if not tutorial_asset:
285-
log.error("'tutorials.zip' not found in the release.")
286+
log.warning("'tutorials.zip' not found in the release.", exc_type=UserWarning)
286287
return []
287288
download_url = tutorial_asset.get('browser_download_url')
288289
if not download_url:
289-
log.error("'browser_download_url' not found for tutorials.zip.")
290+
log.warning("'browser_download_url' not found for tutorials.zip.", exc_type=UserWarning)
290291
return []
291292
return _extract_notebooks_from_asset(download_url)
292293

@@ -369,66 +370,163 @@ def show_version() -> None:
369370
log.print(f'Current easydiffraction v{current_ed_version}')
370371

371372

372-
def is_notebook() -> bool:
373-
"""Determines if the current environment is a Jupyter Notebook.
373+
# TODO: Complete migration to TableRenderer and remove old methods
374+
def render_table(
375+
columns_data,
376+
columns_alignment,
377+
columns_headers=None,
378+
show_index=True,
379+
display_handle=None,
380+
):
381+
del show_index
382+
del display_handle
383+
384+
# Allow callers to pass no headers; synthesize default column names
385+
if columns_headers is None:
386+
num_cols = len(columns_data[0]) if columns_data else 0
387+
columns_headers = [f'col{i + 1}' for i in range(num_cols)]
388+
# If alignment list shorter, pad with 'left'
389+
if len(columns_alignment) < num_cols:
390+
columns_alignment = list(columns_alignment) + ['left'] * (
391+
num_cols - len(columns_alignment)
392+
)
374393

375-
Returns:
376-
bool: True if running inside a Jupyter Notebook, False
377-
otherwise.
378-
"""
379-
if IPython is None:
380-
return False
381-
if is_pycharm(): # Running inside PyCharm
382-
return False
383-
if is_colab(): # Running inside Google Colab
384-
return True
394+
headers = [
395+
(col, align) for col, align in zip(columns_headers, columns_alignment, strict=False)
396+
]
397+
df = pd.DataFrame(columns_data, columns=pd.MultiIndex.from_tuples(headers))
385398

386-
try:
387-
# get_ipython is only defined inside IPython environments
388-
shell = get_ipython().__class__.__name__ # type: ignore[name-defined]
389-
if shell == 'ZMQInteractiveShell': # Jupyter notebook or qtconsole
390-
return True
391-
if shell == 'TerminalInteractiveShell': # Terminal running IPython
392-
return False
393-
# Fallback for any other shell type
394-
return False
395-
except NameError:
396-
return False # Probably standard Python interpreter
399+
tabler = TableRenderer.get()
400+
tabler.render(df)
397401

398402

399-
def is_pycharm() -> bool:
400-
"""Determines if the current environment is PyCharm.
403+
def render_table_old2(
404+
columns_data,
405+
columns_alignment,
406+
columns_headers=None,
407+
show_index=True,
408+
display_handle=None,
409+
):
410+
# TODO: Move log.print(table) to show_table
401411

402-
Returns:
403-
bool: True if running inside PyCharm, False otherwise.
404-
"""
405-
return os.environ.get('PYCHARM_HOSTED') == '1'
412+
# Use pandas DataFrame for Jupyter Notebook rendering
413+
if _env.is_notebook():
414+
# Create DataFrame
415+
if columns_headers is None:
416+
df = pd.DataFrame(columns_data)
417+
df.columns = range(df.shape[1]) # Ensure numeric column labels
418+
columns_headers = df.columns.tolist()
419+
skip_headers = True
420+
else:
421+
df = pd.DataFrame(columns_data, columns=columns_headers)
422+
skip_headers = False
423+
424+
# Force starting index from 1
425+
if show_index:
426+
df.index += 1
406427

428+
# Replace None/NaN values with empty strings
429+
df.fillna('', inplace=True)
407430

408-
def is_colab() -> bool:
409-
"""Determines if the current environment is Google Colab.
431+
# Formatters for data cell alignment and replacing None with
432+
# empty string
433+
def make_formatter(align):
434+
return lambda x: f'<div style="text-align: {align};">{x}</div>'
410435

411-
Returns:
412-
bool: True if running in Google Colab PyCharm, False otherwise.
413-
"""
414-
try:
415-
return find_spec('google.colab') is not None
416-
except ModuleNotFoundError:
417-
return False
436+
formatters = {
437+
col: make_formatter(align)
438+
for col, align in zip(
439+
columns_headers,
440+
columns_alignment,
441+
strict=True,
442+
)
443+
}
444+
445+
# Convert DataFrame to HTML
446+
html = df.to_html(
447+
escape=False,
448+
index=show_index,
449+
formatters=formatters,
450+
border=0,
451+
header=not skip_headers,
452+
)
453+
454+
# Add compact CSS for cells and a custom class to avoid
455+
# affecting other tables
456+
style_block = (
457+
'<style>'
458+
'.ed-tbl th, .ed-tbl td { '
459+
'line-height: 1.9 !important;'
460+
'padding-top: 0.0em !important; '
461+
'padding-bottom: 0.0em !important; '
462+
'padding-left: 0.6em !important; '
463+
'padding-right: 0.6em !important; '
464+
'}'
465+
'.ed-tbl th div, .ed-tbl td div { '
466+
'line-height: 1.9 !important;'
467+
'padding-top: 0.0em !important; '
468+
'padding-bottom: 0.0em !important; '
469+
'padding-left: 0.6em !important; '
470+
'padding-right: 0.6em !important; '
471+
'}'
472+
'.ed-tbl thead th { '
473+
'border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent) '
474+
'!important; }'
475+
'.ed-tbl tbody th { '
476+
'font-weight: normal !important; '
477+
'opacity: 0.5 !important; '
478+
'color: inherit !important; }'
479+
'</style>'
480+
)
481+
html = html.replace(
482+
'<table class="dataframe">',
483+
style_block + '<table class="dataframe ed-tbl" '
484+
'style="'
485+
'border-collapse: separate; '
486+
'border: 1px solid color-mix(in srgb, currentColor 20%, transparent); '
487+
'margin-left: 0.4em;'
488+
'margin-top: 0.5em;'
489+
'margin-bottom: 1em;'
490+
'max-width: 60em;'
491+
'">',
492+
)
493+
494+
# Manually apply text alignment to headers
495+
if not skip_headers:
496+
for col, align in zip(columns_headers, columns_alignment, strict=True):
497+
html = html.replace(f'<th>{col}', f'<th style="text-align: {align};">{col}')
498+
499+
# Display or update the table in Jupyter Notebook
500+
if display_handle is not None:
501+
display_handle.update(HTML(html))
502+
else:
503+
display(HTML(html))
418504

505+
# Use rich for terminal rendering
506+
else:
507+
table = Table(
508+
title=None,
509+
box=box.HEAVY_EDGE,
510+
show_header=True,
511+
header_style='bold blue',
512+
)
419513

420-
def is_github_ci() -> bool:
421-
"""Determines if the current process is running in GitHub Actions
422-
CI.
514+
if columns_headers is not None:
515+
if show_index:
516+
table.add_column(header='#', justify='right', style='dim', no_wrap=True)
517+
for header, alignment in zip(columns_headers, columns_alignment, strict=True):
518+
table.add_column(header=header, justify=alignment, overflow='fold')
423519

424-
Returns:
425-
bool: True if the environment variable ``GITHUB_ACTIONS`` is
426-
set (Always "true" on GitHub Actions), False otherwise.
427-
"""
428-
return os.environ.get('GITHUB_ACTIONS') is not None
520+
for idx, row in enumerate(columns_data, start=1):
521+
if show_index:
522+
table.add_row(str(idx), *map(str, row))
523+
else:
524+
table.add_row(*map(str, row))
429525

526+
log.print(table)
430527

431-
def render_table(
528+
529+
def render_table_old(
432530
columns_data,
433531
columns_alignment,
434532
columns_headers=None,
@@ -448,7 +546,7 @@ def render_table(
448546
display_handle: Optional display handle for updating in Jupyter.
449547
"""
450548
# Use pandas DataFrame for Jupyter Notebook rendering
451-
if is_notebook():
549+
if _env.is_notebook():
452550
# Create DataFrame
453551
if columns_headers is None:
454552
df = pd.DataFrame(columns_data)
@@ -546,7 +644,7 @@ def render_cif(cif_text) -> None:
546644
# Split into lines and replace empty ones with a '&nbsp;'
547645
# (non-breaking space) to force empty lines to be rendered in
548646
# full height in the table. This is only needed in Jupyter Notebook.
549-
if is_notebook():
647+
if _env.is_notebook():
550648
lines: List[str] = [line if line.strip() else '&nbsp;' for line in cif_text.splitlines()]
551649
else:
552650
lines: List[str] = [line for line in cif_text.splitlines()]

0 commit comments

Comments
 (0)