From b6e45d5fb69057d3df28f50a213687d43be7320d Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Fri, 14 Nov 2025 15:47:09 +0100 Subject: [PATCH 1/2] Improvements to the `by_name()` method; fixes #69 --- .../src/additional_methods/by_name.py.txt | 52 ++++++++++++++++--- pipeline/tests/test_regressions.py | 31 +++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/pipeline/src/additional_methods/by_name.py.txt b/pipeline/src/additional_methods/by_name.py.txt index fb96249a..25e7ae8a 100644 --- a/pipeline/src/additional_methods/by_name.py.txt +++ b/pipeline/src/additional_methods/by_name.py.txt @@ -3,12 +3,52 @@ return [value for value in cls.__dict__.values() if isinstance(value, cls)] @classmethod - def by_name(cls, name): + def by_name( + cls, + name: str, + match: str = "equals", + all: bool = False, + ): + """ + Search for instances in the openMINDS instance library based on their name. + + This includes properties "name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation", and "synonyms". + + Note that not all metadata classes have a name. + + Args: + name (str): a string to search for. + match (str, optional): either "equals" (exact match - default) or "contains". + all (bool, optional): Whether to return all objects that match the name, or only the first. Defaults to False. + """ + namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation", "synonyms") if cls._instance_lookup is None: cls._instance_lookup = {} for instance in cls.instances(): - cls._instance_lookup[instance.name] = instance - if instance.synonyms: - for synonym in instance.synonyms: - cls._instance_lookup[synonym] = instance - return cls._instance_lookup[name] + keys = [] + for prop_name in namelike_properties[:-1]: # handle 'synonyms' separately + if hasattr(instance, prop_name): + keys.append(getattr(instance, prop_name)) + if hasattr(instance, "synonyms"): + for synonym in instance.synonyms or []: + keys.append(synonym) + for key in keys: + if key in cls._instance_lookup: + cls._instance_lookup[key].append(instance) + else: + cls._instance_lookup[key] = [instance] + if match == "equals": + matches = cls._instance_lookup.get(name, None) + elif match == "contains": + matches = [] + for key, instances in cls._instance_lookup.items(): + if name in key: + matches.extend(instances) + else: + raise ValueError("'match' must be either 'exact' or 'contains'") + if all: + return matches + elif len(matches) > 0: + return matches[0] + else: + return None diff --git a/pipeline/tests/test_regressions.py b/pipeline/tests/test_regressions.py index aa65d254..359df163 100644 --- a/pipeline/tests/test_regressions.py +++ b/pipeline/tests/test_regressions.py @@ -300,3 +300,34 @@ def test_issue0073b(om): ds1.is_variant_of = ds2 failures = ds1.validate() + + +@pytest.mark.parametrize("om", [openminds.latest]) +def test_issue0069(om): + # https://github.com/openMetadataInitiative/openMINDS_Python/issues/69 + # The License class has a classmethod "by_name()" which assumes License is a controlled term + # (i.e., it has properties "name" and "synonyms"). + # However License does not have these properties, it has "short_name" and "full_name". + + # Test with default arguments (single result, exact match) + result = om.core.License.by_name("CC-BY-4.0") + assert result.short_name == "CC-BY-4.0" + + result = om.sands.ParcellationEntity.by_name("NODa,b") + assert result.abbreviation == "NODa,b" + + result = om.sands.CommonCoordinateSpace.by_name("MEBRAINS population-based monkey brain template") + assert result.full_name == "MEBRAINS population-based monkey brain template" + + assert om.controlled_terms.BiologicalOrder.by_name("rodents") == om.controlled_terms.BiologicalOrder.by_name("Rodentia") != None + + # Test with "all=True" + results = om.sands.BrainAtlasVersion.by_name("Julich-Brain Atlas", all=True) + assert len(results) == 30 + assert all(r.short_name == "Julich-Brain Atlas" for r in results) + assert len(set(r.id for r in results)) == len(results) + + # Test with "match='contains'" + results = om.core.License.by_name("Creative Commons", all=True, match="contains") + assert len(results) == 7 + assert all("CC" in r.short_name for r in results) From 5a40cb1c556051ef087ed29841a2fe67706d9c19 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 18 Nov 2025 11:12:20 +0100 Subject: [PATCH 2/2] implement suggestions from @Raphael-Gazzotti --- pipeline/src/additional_methods/by_name.py.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pipeline/src/additional_methods/by_name.py.txt b/pipeline/src/additional_methods/by_name.py.txt index 25e7ae8a..13491111 100644 --- a/pipeline/src/additional_methods/by_name.py.txt +++ b/pipeline/src/additional_methods/by_name.py.txt @@ -21,12 +21,12 @@ match (str, optional): either "equals" (exact match - default) or "contains". all (bool, optional): Whether to return all objects that match the name, or only the first. Defaults to False. """ - namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation", "synonyms") + namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation") if cls._instance_lookup is None: cls._instance_lookup = {} for instance in cls.instances(): keys = [] - for prop_name in namelike_properties[:-1]: # handle 'synonyms' separately + for prop_name in namelike_properties: if hasattr(instance, prop_name): keys.append(getattr(instance, prop_name)) if hasattr(instance, "synonyms"): @@ -45,7 +45,7 @@ if name in key: matches.extend(instances) else: - raise ValueError("'match' must be either 'exact' or 'contains'") + raise ValueError("'match' must be either 'equals' or 'contains'") if all: return matches elif len(matches) > 0: