From 14aeef06522617596006e20c64c31ed63c94faa9 Mon Sep 17 00:00:00 2001 From: ilkankilic Date: Thu, 23 Apr 2026 15:25:41 +0200 Subject: [PATCH 1/3] test: add case-insensitive morphology path test --- tests/test_cell/test_morphology_wrapper.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cell/test_morphology_wrapper.py b/tests/test_cell/test_morphology_wrapper.py index 03e7c17c..444b6d78 100644 --- a/tests/test_cell/test_morphology_wrapper.py +++ b/tests/test_cell/test_morphology_wrapper.py @@ -59,6 +59,19 @@ def test_split_morphology_path_container_with_cell_name(self, tmp_path): assert morph_name == "cell_name" assert morph_ext == "" + def test_case_insensitive_morphology_path(tmp_path): + """Morphology loading should be case-insensitive.""" + p = tmp_path / "cell.ASC" + p.write_text("dummy") + + wrong_case = tmp_path / "cell.asc" + + wrapper = MorphIOWrapper(str(wrong_case)) + + # Stronger checks + assert wrapper._morph_name == "cell" + assert wrapper._morph_ext.lower() == ".asc" + def test_morphology_wrapper_init_success(self): """Test successful MorphologyWrapper initialization with real H5 file.""" # Test with a real H5 file from BlueCelluLab test data From 860d1ee905751412a52c3ed52fe20551c514ae29 Mon Sep 17 00:00:00 2001 From: ilkankilic Date: Thu, 23 Apr 2026 15:47:53 +0200 Subject: [PATCH 2/3] fix: support case-insensitive morphology file paths and extensions --- bluecellulab/cell/morphio_wrapper.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bluecellulab/cell/morphio_wrapper.py b/bluecellulab/cell/morphio_wrapper.py index fea7f00a..43de4d8d 100644 --- a/bluecellulab/cell/morphio_wrapper.py +++ b/bluecellulab/cell/morphio_wrapper.py @@ -16,6 +16,8 @@ from numpy.linalg import eig, norm from bluecellulab.exceptions import BluecellulabError +from pathlib import Path + logger = logging.getLogger(__name__) @@ -293,6 +295,22 @@ def __str__(self): return f"{self.name}[{self.id}]" +def resolve_case_insensitive_path(path: str) -> str: + p = Path(path) + if p.exists(): + return str(p) + + parent = p.parent + if not parent.exists(): + return path + + for child in parent.iterdir(): + if child.name.lower() == p.name.lower(): + return str(child) + + return path + + class MorphIOWrapper: """Load a MorphIO morphology and generate HOC instantiation commands. @@ -324,7 +342,9 @@ def __init__(self, input_file, options=0): options: Additional ``morphio.Option`` flags OR-ed into ``Option.nrn_order`` when loading. Defaults to ``0``. """ + input_file = resolve_case_insensitive_path(str(input_file)) self._collection_dir, self._morph_name, self._morph_ext = split_morphology_path(input_file) + self._morph_ext = self._morph_ext.lower() self._options = options self._build_morph() # This logic is similar to what's in BaseCell, but at this point we are still From dbaea45645c058ed9ce256456c50a3c75b0c16d2 Mon Sep 17 00:00:00 2001 From: ilkankilic Date: Thu, 23 Apr 2026 16:34:23 +0200 Subject: [PATCH 3/3] fix: preserve case for H5 container cell names while resolving paths --- bluecellulab/cell/morphio_wrapper.py | 15 ++++++++- tests/test_cell/test_morphology_wrapper.py | 36 ++++++++++++++++++---- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/bluecellulab/cell/morphio_wrapper.py b/bluecellulab/cell/morphio_wrapper.py index 43de4d8d..20b53f3b 100644 --- a/bluecellulab/cell/morphio_wrapper.py +++ b/bluecellulab/cell/morphio_wrapper.py @@ -311,6 +311,17 @@ def resolve_case_insensitive_path(path: str) -> str: return path +def is_h5_container_path(path: str) -> bool: + candidate = path + while not os.path.exists(candidate): + parent = os.path.dirname(candidate) + if parent == candidate: + return False + candidate = parent + + return os.path.isfile(candidate) and candidate.lower().endswith(".h5") + + class MorphIOWrapper: """Load a MorphIO morphology and generate HOC instantiation commands. @@ -342,7 +353,9 @@ def __init__(self, input_file, options=0): options: Additional ``morphio.Option`` flags OR-ed into ``Option.nrn_order`` when loading. Defaults to ``0``. """ - input_file = resolve_case_insensitive_path(str(input_file)) + input_file = str(input_file) + if not is_h5_container_path(input_file): + input_file = resolve_case_insensitive_path(input_file) self._collection_dir, self._morph_name, self._morph_ext = split_morphology_path(input_file) self._morph_ext = self._morph_ext.lower() self._options = options diff --git a/tests/test_cell/test_morphology_wrapper.py b/tests/test_cell/test_morphology_wrapper.py index 444b6d78..9bf4aca0 100644 --- a/tests/test_cell/test_morphology_wrapper.py +++ b/tests/test_cell/test_morphology_wrapper.py @@ -59,18 +59,42 @@ def test_split_morphology_path_container_with_cell_name(self, tmp_path): assert morph_name == "cell_name" assert morph_ext == "" - def test_case_insensitive_morphology_path(tmp_path): - """Morphology loading should be case-insensitive.""" + def test_case_insensitive_morphology_path(self, tmp_path): p = tmp_path / "cell.ASC" p.write_text("dummy") - wrong_case = tmp_path / "cell.asc" + with patch.object(MorphIOWrapper, "_build_morph"): + with patch.object(MorphIOWrapper, "_get_section_names", return_value=[]): + with patch.object(MorphIOWrapper, "_build_sec_typeid_distrib"): + wrapper = MorphIOWrapper(str(tmp_path / "cell.asc")) - wrapper = MorphIOWrapper(str(wrong_case)) + assert wrapper._morph_name == "cell" + assert wrapper._morph_ext == ".asc" + + def test_case_insensitive_morphology_path_reverse(self, tmp_path): + """Should also work if file is lowercase and input is uppercase.""" + p = tmp_path / "cell.asc" + p.write_text("dummy") + + with patch.object(MorphIOWrapper, "_build_morph"): + with patch.object(MorphIOWrapper, "_get_section_names", return_value=[]): + with patch.object(MorphIOWrapper, "_build_sec_typeid_distrib"): + wrapper = MorphIOWrapper(str(tmp_path / "cell.ASC")) - # Stronger checks assert wrapper._morph_name == "cell" - assert wrapper._morph_ext.lower() == ".asc" + assert wrapper._morph_ext == ".asc" + + def test_extension_normalization(self, tmp_path): + """Extension should always be normalized to lowercase.""" + p = tmp_path / "cell.SWC" + p.write_text("dummy") + + with patch.object(MorphIOWrapper, "_build_morph"): + with patch.object(MorphIOWrapper, "_get_section_names", return_value=[]): + with patch.object(MorphIOWrapper, "_build_sec_typeid_distrib"): + wrapper = MorphIOWrapper(str(p)) + + assert wrapper._morph_ext == ".swc" def test_morphology_wrapper_init_success(self): """Test successful MorphologyWrapper initialization with real H5 file."""