2323from uncertainties import ufloat
2424from uncertainties import ufloat_fromstr
2525
26- import easydiffraction .utils .env as _env
26+ import easydiffraction .utils .env as _env # TODO: Rename to environment?
2727from easydiffraction import log
2828from 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 ' '
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 ' ' for line in cif_text .splitlines ()]
551649 else :
552650 lines : List [str ] = [line for line in cif_text .splitlines ()]
0 commit comments