diff --git a/src/easydiffraction/io/cif/parse.py b/src/easydiffraction/io/cif/parse.py index 0577f0b47..9cf46118b 100644 --- a/src/easydiffraction/io/cif/parse.py +++ b/src/easydiffraction/io/cif/parse.py @@ -4,11 +4,18 @@ from __future__ import annotations +import re + import gemmi # Minimum raw-string length for CIF surrounding-quote detection _MIN_QUOTED_LEN = 2 +# Model datablock names use a backend-safe subset of CIF block-name +# characters. Hyphens and underscores are retained because they are used +# throughout EasyDiffraction's public naming conventions. +_UNSUPPORTED_DATABLOCK_NAME_CHARS = re.compile(r'[^a-z0-9_-]') + def document_from_path(path: str) -> gemmi.cif.Document: """Read a CIF document from a file path.""" @@ -26,9 +33,9 @@ def pick_sole_block(doc: gemmi.cif.Document) -> gemmi.cif.Block: def name_from_block(block: gemmi.cif.Block) -> str: - """Extract a model name from the CIF block name.""" - # TODO: Need validator or normalization? - return block.name + """Extract and normalize a model name from the CIF block name.""" + lowercase_name = block.name.lower() + return _UNSUPPORTED_DATABLOCK_NAME_CHARS.sub('', lowercase_name) def read_cif_str(block: gemmi.cif.Block, tag: str) -> str | None: diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_factory.py index f58b117f7..90af7f1a7 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/item/test_factory.py +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_factory.py @@ -29,6 +29,15 @@ def test_experiment_factory_from_scratch(): assert ex.experiment_type.sample_form.value == SampleFormEnum.POWDER.value +def test_from_cif_str_normalizes_datablock_name(): + from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory + + experiment = ExperimentFactory.from_cif_str('data_83267-ICSD\n') + + assert experiment.name == '83267-icsd' + assert experiment.as_cif.startswith('data_83267-icsd\n') + + def test_from_cif_str_restores_non_default_peak_profile_type(): """ Loading a CIF with a non-default peak profile type must reconstruct diff --git a/tests/unit/easydiffraction/datablocks/structure/item/test_factory.py b/tests/unit/easydiffraction/datablocks/structure/item/test_factory.py index 0378f1339..1f85b891e 100644 --- a/tests/unit/easydiffraction/datablocks/structure/item/test_factory.py +++ b/tests/unit/easydiffraction/datablocks/structure/item/test_factory.py @@ -9,6 +9,13 @@ def test_from_scratch(): assert m.name == 'abc' +def test_from_cif_str_normalizes_datablock_name(): + structure = StructureFactory.from_cif_str('data_83267-ICSD\n') + + assert structure.name == '83267-icsd' + assert structure.as_cif.startswith('data_83267-icsd\n') + + def test_from_cif_str_accepts_underscore_style_structure_tags(): cif = """\ data_legacy diff --git a/tests/unit/easydiffraction/io/cif/test_parse.py b/tests/unit/easydiffraction/io/cif/test_parse.py index f1d37bbe1..c46ef6c58 100644 --- a/tests/unit/easydiffraction/io/cif/test_parse.py +++ b/tests/unit/easydiffraction/io/cif/test_parse.py @@ -31,6 +31,17 @@ def test_name_from_block(self): name = name_from_block(block) assert name == 'silicon' + def test_name_from_block_normalizes_unsupported_name(self): + from easydiffraction.io.cif.parse import document_from_string + from easydiffraction.io.cif.parse import name_from_block + from easydiffraction.io.cif.parse import pick_sole_block + + cif = 'data_My+83267-ICSD.example\n_cell.length_a 5.43\n' + doc = document_from_string(cif) + block = pick_sole_block(doc) + name = name_from_block(block) + assert name == 'my83267-icsdexample' + class TestDocumentFromPath: def test_valid_file(self, tmp_path):