batplot 1.7.26__tar.gz → 1.7.28__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of batplot might be problematic. Click here for more details.
- {batplot-1.7.26/batplot.egg-info → batplot-1.7.28}/PKG-INFO +1 -1
- {batplot-1.7.26 → batplot-1.7.28}/batplot/__init__.py +1 -1
- {batplot-1.7.26 → batplot-1.7.28}/batplot/cpc_interactive.py +4 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/electrochem_interactive.py +4 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/interactive.py +4 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/operando.py +3 -3
- {batplot-1.7.26 → batplot-1.7.28}/batplot/operando_ec_interactive.py +89 -2
- {batplot-1.7.26 → batplot-1.7.28}/batplot/session.py +18 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/style.py +4 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/utils.py +48 -0
- {batplot-1.7.26 → batplot-1.7.28/batplot.egg-info}/PKG-INFO +1 -1
- {batplot-1.7.26 → batplot-1.7.28}/pyproject.toml +1 -1
- {batplot-1.7.26 → batplot-1.7.28}/LICENSE +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/MANIFEST.in +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/README.md +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/USER_MANUAL.md +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/args.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/batch.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/batplot.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/cif.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/cli.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/color_utils.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/config.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/converters.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/data/USER_MANUAL.md +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/manual.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/modes.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/plotting.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/readers.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/ui.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot/version_check.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/__init__.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/args.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/batch.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/batplot.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/cif.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/cli.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/color_utils.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/config.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/converters.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/interactive.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/modes.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/operando.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/plotting.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/readers.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/session.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/style.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/ui.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/utils.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/batplot_backup_20251121_223043/version_check.py +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/setup.cfg +0 -0
- {batplot-1.7.26 → batplot-1.7.28}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.28
|
|
4
4
|
Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
|
|
5
5
|
Author-email: Tian Dai <tianda@uio.no>
|
|
6
6
|
License: MIT License
|
|
@@ -2090,6 +2090,10 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
|
|
|
2090
2090
|
if yn != 'y':
|
|
2091
2091
|
_print_menu(); continue
|
|
2092
2092
|
if target:
|
|
2093
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
2094
|
+
from .utils import ensure_exact_case_filename
|
|
2095
|
+
target = ensure_exact_case_filename(target)
|
|
2096
|
+
|
|
2093
2097
|
# Save current legend position before export (savefig can change layout)
|
|
2094
2098
|
saved_legend_pos = None
|
|
2095
2099
|
try:
|
|
@@ -1684,6 +1684,10 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
|
|
|
1684
1684
|
if not already_confirmed and os.path.exists(target):
|
|
1685
1685
|
target = _confirm_overwrite(target)
|
|
1686
1686
|
if target:
|
|
1687
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
1688
|
+
from .utils import ensure_exact_case_filename
|
|
1689
|
+
target = ensure_exact_case_filename(target)
|
|
1690
|
+
|
|
1687
1691
|
# Save current legend position before export (savefig can change layout)
|
|
1688
1692
|
saved_legend_pos = None
|
|
1689
1693
|
try:
|
|
@@ -3709,6 +3709,10 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3709
3709
|
if not export_target:
|
|
3710
3710
|
print("Export canceled.")
|
|
3711
3711
|
else:
|
|
3712
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
3713
|
+
from .utils import ensure_exact_case_filename
|
|
3714
|
+
export_target = ensure_exact_case_filename(export_target)
|
|
3715
|
+
|
|
3712
3716
|
# Temporarily remove numbering for export
|
|
3713
3717
|
for i, txt in enumerate(label_text_objects):
|
|
3714
3718
|
txt.set_text(labels[i])
|
|
@@ -386,7 +386,7 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
386
386
|
x_data, y_data, current_mA, x_label, y_label = result
|
|
387
387
|
# For EC-Lab files: x_label='Time (h)', y_label='Voltage (V)'
|
|
388
388
|
# For simple files: x_label could be 'Time(h)', 'time', etc.
|
|
389
|
-
# EC-Lab
|
|
389
|
+
# EC-Lab files: read_mpt_file already converts time from seconds to hours
|
|
390
390
|
# operando plots with voltage on X-axis and time on Y-axis
|
|
391
391
|
|
|
392
392
|
# Check if labels indicate time/voltage data (flexible matching)
|
|
@@ -400,8 +400,8 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
400
400
|
is_time_voltage = (has_time_in_x or has_time_in_y) and (has_voltage_in_x or has_voltage_in_y)
|
|
401
401
|
|
|
402
402
|
if x_label == 'Time (h)' and y_label == 'Voltage (V)':
|
|
403
|
-
# EC-Lab file:
|
|
404
|
-
time_h = np.asarray(x_data, float)
|
|
403
|
+
# EC-Lab file: time is already in hours from read_mpt_file, just swap axes
|
|
404
|
+
time_h = np.asarray(x_data, float) # Already in hours, no conversion needed
|
|
405
405
|
voltage_v = np.asarray(y_data, float)
|
|
406
406
|
x_data = voltage_v
|
|
407
407
|
y_data = time_h
|
|
@@ -344,6 +344,64 @@ def _update_custom_colorbar(cbar_ax, im=None, label=None, label_mode=None):
|
|
|
344
344
|
_draw_custom_colorbar(cbar_ax, im, label, label_mode)
|
|
345
345
|
|
|
346
346
|
|
|
347
|
+
def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
|
|
348
|
+
"""Detach a Matplotlib Colorbar from its mappable callbacks.
|
|
349
|
+
|
|
350
|
+
Why this exists:
|
|
351
|
+
- In this interactive menu we draw a *custom* colorbar by clearing/redrawing `cbar.ax`.
|
|
352
|
+
- If `cbar` is a real `matplotlib.colorbar.Colorbar` (e.g., loaded from a session),
|
|
353
|
+
it remains connected to `im` via `im.callbacksSM`. Subsequent `im.set_clim()` /
|
|
354
|
+
`im.set_cmap()` triggers `Colorbar.update_normal()`, which can crash after we
|
|
355
|
+
cleared/redrew the axes (observed as: NotImplementedError: cannot remove artist).
|
|
356
|
+
- We therefore disconnect that callback once and always update the custom colorbar
|
|
357
|
+
via `_update_custom_colorbar(...)`.
|
|
358
|
+
"""
|
|
359
|
+
try:
|
|
360
|
+
if cbar is None or im is None:
|
|
361
|
+
return
|
|
362
|
+
cax = getattr(cbar, 'ax', None)
|
|
363
|
+
if cax is not None and getattr(cax, '_bp_detached_mpl_colorbar', False):
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Matplotlib's Colorbar stores the callback id as `_cid` (most versions).
|
|
367
|
+
cid = None
|
|
368
|
+
for attr in ('_cid', '_cid_colorbar', 'cid'):
|
|
369
|
+
try:
|
|
370
|
+
v = getattr(cbar, attr, None)
|
|
371
|
+
if isinstance(v, int):
|
|
372
|
+
cid = v
|
|
373
|
+
break
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
# Disconnect from the ScalarMappable callback registry.
|
|
378
|
+
if cid is not None:
|
|
379
|
+
try:
|
|
380
|
+
cbreg = getattr(im, 'callbacksSM', None)
|
|
381
|
+
if cbreg is not None and hasattr(cbreg, 'disconnect'):
|
|
382
|
+
cbreg.disconnect(cid)
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
# Prevent future built-in updates (best-effort; safe for mock colorbars too).
|
|
387
|
+
try:
|
|
388
|
+
if hasattr(cbar, 'mappable'):
|
|
389
|
+
cbar.mappable = None
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
try:
|
|
393
|
+
if hasattr(cbar, 'solids'):
|
|
394
|
+
cbar.solids = None
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
if cax is not None:
|
|
399
|
+
setattr(cax, '_bp_detached_mpl_colorbar', True)
|
|
400
|
+
except Exception:
|
|
401
|
+
# Never let detaching break the interactive menu.
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
|
|
347
405
|
def _ensure_fixed_params(fig, ax, cbar_ax, ec_ax):
|
|
348
406
|
"""Initialize and return fixed geometry parameters in inches.
|
|
349
407
|
|
|
@@ -540,6 +598,11 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
540
598
|
# Normalize file path list for downstream helpers
|
|
541
599
|
file_paths = list(file_paths) if file_paths else []
|
|
542
600
|
|
|
601
|
+
# If we were given a real Matplotlib Colorbar (e.g. from session load),
|
|
602
|
+
# detach it from `im` immediately. This must happen before any function
|
|
603
|
+
# that may clear/redraw `cbar.ax` (custom colorbar) is called.
|
|
604
|
+
_detach_mpl_colorbar_callbacks(cbar, im)
|
|
605
|
+
|
|
543
606
|
def _renormalize_to_visible():
|
|
544
607
|
"""Adjust color scale to match the intensity range of the currently visible region.
|
|
545
608
|
|
|
@@ -904,6 +967,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
904
967
|
# Initialize custom colorbar (replaces matplotlib's colorbar)
|
|
905
968
|
cbar_label = getattr(cbar.ax, '_colorbar_label', 'Intensity')
|
|
906
969
|
cbar_label_mode = getattr(fig, '_colorbar_label_mode', 'normal')
|
|
970
|
+
# If we were given a real Matplotlib Colorbar (e.g. from session load),
|
|
971
|
+
# detach it from `im` before we clear/redraw the axes for the custom colorbar.
|
|
972
|
+
_detach_mpl_colorbar_callbacks(cbar, im)
|
|
907
973
|
_draw_custom_colorbar(cbar.ax, im, cbar_label, cbar_label_mode)
|
|
908
974
|
# Decrease distance between operando and EC plots once per session
|
|
909
975
|
if not getattr(ec_ax, '_ec_gap_adjusted', False):
|
|
@@ -1204,7 +1270,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1204
1270
|
if snap.get('cmap'):
|
|
1205
1271
|
im.set_cmap(snap['cmap'])
|
|
1206
1272
|
if cbar is not None:
|
|
1207
|
-
cbar.
|
|
1273
|
+
_update_custom_colorbar(cbar.ax, im)
|
|
1208
1274
|
except Exception:
|
|
1209
1275
|
pass
|
|
1210
1276
|
# Restore colorbar side (ticks/label) and redraw custom colorbar to keep position
|
|
@@ -1765,6 +1831,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
1765
1831
|
target = _co(target)
|
|
1766
1832
|
if not target:
|
|
1767
1833
|
print_menu(); continue
|
|
1834
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
1835
|
+
from .utils import ensure_exact_case_filename
|
|
1836
|
+
target = ensure_exact_case_filename(target)
|
|
1837
|
+
|
|
1768
1838
|
_, ext = os.path.splitext(target)
|
|
1769
1839
|
if ext.lower() == '.svg':
|
|
1770
1840
|
try:
|
|
@@ -2052,6 +2122,19 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
2052
2122
|
print_menu(); continue
|
|
2053
2123
|
dump_operando_session(target, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
|
|
2054
2124
|
fig._last_session_save_path = target
|
|
2125
|
+
# Show the actual filename that was saved (in case of case differences on macOS)
|
|
2126
|
+
actual_name = os.path.basename(target)
|
|
2127
|
+
if os.path.exists(target):
|
|
2128
|
+
# Get the actual filename as stored on disk (for case-sensitive display)
|
|
2129
|
+
try:
|
|
2130
|
+
dir_files = os.listdir(folder)
|
|
2131
|
+
for f in dir_files:
|
|
2132
|
+
if f.lower() == actual_name.lower():
|
|
2133
|
+
actual_name = f
|
|
2134
|
+
break
|
|
2135
|
+
except Exception:
|
|
2136
|
+
pass
|
|
2137
|
+
print(f"Operando session saved to {os.path.join(folder, actual_name)}")
|
|
2055
2138
|
except Exception as e:
|
|
2056
2139
|
print(f"Save failed: {e}")
|
|
2057
2140
|
print_menu(); continue
|
|
@@ -3777,6 +3860,10 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3777
3860
|
if yn != 'y':
|
|
3778
3861
|
target = None
|
|
3779
3862
|
if target:
|
|
3863
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
3864
|
+
from .utils import ensure_exact_case_filename
|
|
3865
|
+
target = ensure_exact_case_filename(target)
|
|
3866
|
+
|
|
3780
3867
|
with open(target, 'w', encoding='utf-8') as f:
|
|
3781
3868
|
json.dump(cfg, f, indent=2)
|
|
3782
3869
|
print(f"Exported style to {target}")
|
|
@@ -3900,7 +3987,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
|
|
|
3900
3987
|
try:
|
|
3901
3988
|
im.set_cmap(cmap)
|
|
3902
3989
|
if cbar is not None:
|
|
3903
|
-
cbar.
|
|
3990
|
+
_update_custom_colorbar(cbar.ax, im)
|
|
3904
3991
|
except Exception:
|
|
3905
3992
|
pass
|
|
3906
3993
|
|
|
@@ -519,6 +519,10 @@ def dump_session(
|
|
|
519
519
|
if not target:
|
|
520
520
|
print("Session save canceled.")
|
|
521
521
|
return
|
|
522
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
523
|
+
from .utils import ensure_exact_case_filename
|
|
524
|
+
target = ensure_exact_case_filename(target)
|
|
525
|
+
|
|
522
526
|
with open(target, 'wb') as f:
|
|
523
527
|
pickle.dump(sess, f)
|
|
524
528
|
print(f"Session saved to {target}")
|
|
@@ -663,6 +667,16 @@ def dump_operando_session(
|
|
|
663
667
|
# Capture operando WASD state, spines, and tick widths
|
|
664
668
|
op_wasd_state = _capture_wasd_state(ax)
|
|
665
669
|
op_spines, op_ticks = _capture_spine_tick_widths(ax)
|
|
670
|
+
|
|
671
|
+
# Capture operando title offsets
|
|
672
|
+
op_title_offsets = {
|
|
673
|
+
'top_y': float(getattr(ax, '_top_xlabel_manual_offset_y_pts', 0.0) or 0.0),
|
|
674
|
+
'top_x': float(getattr(ax, '_top_xlabel_manual_offset_x_pts', 0.0) or 0.0),
|
|
675
|
+
'bottom_y': float(getattr(ax, '_bottom_xlabel_manual_offset_y_pts', 0.0) or 0.0),
|
|
676
|
+
'left_x': float(getattr(ax, '_left_ylabel_manual_offset_x_pts', 0.0) or 0.0),
|
|
677
|
+
'right_x': float(getattr(ax, '_right_ylabel_manual_offset_x_pts', 0.0) or 0.0),
|
|
678
|
+
'right_y': float(getattr(ax, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0),
|
|
679
|
+
}
|
|
666
680
|
|
|
667
681
|
# EC panel (optional)
|
|
668
682
|
ec_state = None
|
|
@@ -783,6 +797,10 @@ def dump_operando_session(
|
|
|
783
797
|
if not target:
|
|
784
798
|
print("Session save canceled.")
|
|
785
799
|
return
|
|
800
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
801
|
+
from .utils import ensure_exact_case_filename
|
|
802
|
+
target = ensure_exact_case_filename(target)
|
|
803
|
+
|
|
786
804
|
with open(target, 'wb') as f:
|
|
787
805
|
pickle.dump(sess, f)
|
|
788
806
|
print(f"Operando session saved to {target}")
|
|
@@ -787,6 +787,10 @@ def export_style_config(
|
|
|
787
787
|
print("Style export canceled.")
|
|
788
788
|
return None
|
|
789
789
|
|
|
790
|
+
# Ensure exact case is preserved (important for macOS case-insensitive filesystem)
|
|
791
|
+
from .utils import ensure_exact_case_filename
|
|
792
|
+
target_path = ensure_exact_case_filename(target_path)
|
|
793
|
+
|
|
790
794
|
with open(target_path, "w", encoding="utf-8") as f:
|
|
791
795
|
json.dump(cfg, f, indent=2)
|
|
792
796
|
print(f"Exported style to {target_path}")
|
|
@@ -777,6 +777,54 @@ def choose_save_path(file_paths: list, purpose: str = "saving") -> Optional[str]
|
|
|
777
777
|
return os.getcwd()
|
|
778
778
|
|
|
779
779
|
|
|
780
|
+
def ensure_exact_case_filename(target_path: str) -> str:
|
|
781
|
+
"""Ensure a file is saved with the exact case specified, even on case-insensitive filesystems.
|
|
782
|
+
|
|
783
|
+
This function handles case-insensitive filesystems (macOS, Windows) by ensuring that
|
|
784
|
+
if a file exists with different case, it is removed first so the new file can be created
|
|
785
|
+
with the exact case specified by the user.
|
|
786
|
+
|
|
787
|
+
On case-sensitive filesystems (Linux, Unix), this function is safe but has no effect
|
|
788
|
+
since files with different case are treated as different files.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
target_path: The desired file path with exact case
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
The same path (for compatibility)
|
|
795
|
+
"""
|
|
796
|
+
folder = os.path.dirname(target_path)
|
|
797
|
+
desired_basename = os.path.basename(target_path)
|
|
798
|
+
|
|
799
|
+
if not folder or not desired_basename:
|
|
800
|
+
return target_path
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
# Check if file already exists with exact case
|
|
804
|
+
if os.path.exists(target_path):
|
|
805
|
+
# Check if the actual filename on disk matches the desired case
|
|
806
|
+
existing_files = os.listdir(folder)
|
|
807
|
+
for existing_file in existing_files:
|
|
808
|
+
# If same name (case-insensitive) but different case, we need to fix it
|
|
809
|
+
if existing_file.lower() == desired_basename.lower() and existing_file != desired_basename:
|
|
810
|
+
existing_path = os.path.join(folder, existing_file)
|
|
811
|
+
# Delete the existing file with wrong case
|
|
812
|
+
# This is safe on case-insensitive filesystems and has no effect on case-sensitive ones
|
|
813
|
+
try:
|
|
814
|
+
if os.path.exists(existing_path):
|
|
815
|
+
os.remove(existing_path)
|
|
816
|
+
except Exception:
|
|
817
|
+
# Ignore errors (e.g., permission issues, file in use)
|
|
818
|
+
pass
|
|
819
|
+
break
|
|
820
|
+
except Exception:
|
|
821
|
+
# If we can't check/list the directory, just return the path as-is
|
|
822
|
+
# This is safe and ensures we don't break on permission errors
|
|
823
|
+
pass
|
|
824
|
+
|
|
825
|
+
return target_path
|
|
826
|
+
|
|
827
|
+
|
|
780
828
|
def _normalize_extension(ext: str) -> str:
|
|
781
829
|
if not ext:
|
|
782
830
|
return ext
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.28
|
|
4
4
|
Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
|
|
5
5
|
Author-email: Tian Dai <tianda@uio.no>
|
|
6
6
|
License: MIT License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "batplot"
|
|
7
|
-
version = "1.7.
|
|
7
|
+
version = "1.7.28"
|
|
8
8
|
description = "Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Tian Dai", email = "tianda@uio.no" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|