From 5c5e7a0a5d65243f87c24a3e069f92ea9b4f9396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Genevi=C3=A8ve=20Fleury?= <113036747+gefleury@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:12:29 +0200 Subject: [PATCH 1/4] Add ignore_accents option to by_name() by_name() gains an ignore_accents parameter (default False). When True, accents and other diacritical marks are stripped before matching, The generated modules import unicodedata for this. --- .../src/additional_methods/by_name.py.txt | 17 ++++++++++++--- pipeline/tests/test_regressions.py | 21 +++++++++++++++++++ pipeline/translator.py | 6 +++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/pipeline/src/additional_methods/by_name.py.txt b/pipeline/src/additional_methods/by_name.py.txt index a7515ee3..6fc1216c 100644 --- a/pipeline/src/additional_methods/by_name.py.txt +++ b/pipeline/src/additional_methods/by_name.py.txt @@ -9,6 +9,7 @@ match: str = "equals", all: bool = False, case_sensitive: bool = True, + ignore_accents: bool = False, ): """ Search for instances in the openMINDS instance library based on their name. @@ -24,6 +25,8 @@ (the given string contains the name-like property). all (bool, optional): Whether to return all objects that match the name, or only the first. Defaults to False. case_sensitive (bool, optional): Whether the search should be case-sensitive. Defaults to True. + ignore_accents (bool, optional): Whether to ignore accents (acute, grave, circumflex) and + other diacritical marks (cedilla, tilde, ring, etc.) when matching. Defaults to False. """ namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation") if cls._instance_lookup is None: @@ -43,16 +46,24 @@ else: cls._instance_lookup[key] = [instance] + def remove_accents(s): + nfd_form = unicodedata.normalize("NFD", s) + return "".join(c for c in nfd_form if not unicodedata.combining(c)) + def normalize(s): - return s if case_sensitive else s.casefold() + if not case_sensitive: + s = s.casefold() + if ignore_accents: + s = remove_accents(s) + return s if match == "equals": - if case_sensitive: + if case_sensitive and not ignore_accents: matches = cls._instance_lookup.get(name, []) else: matches = [] for key, instances in cls._instance_lookup.items(): - if key.casefold() == name.casefold(): + if normalize(key) == normalize(name): matches.extend(instances) elif match == "contains": matches = [] diff --git a/pipeline/tests/test_regressions.py b/pipeline/tests/test_regressions.py index a2d34ca8..62c28d45 100644 --- a/pipeline/tests/test_regressions.py +++ b/pipeline/tests/test_regressions.py @@ -728,3 +728,24 @@ def test_pr0100_by_name_match_within(om): # but none of those full names is itself a substring of "Macaca". assert Species.by_name("Macaca", match="contains", all=True) is not None assert Species.by_name("Macaca", match="within", all=True) is None + + +@pytest.mark.parametrize("om", [openminds.latest]) +def test_prXXXX_by_name_ignore_accents(om): + # https://github.com/openMetadataInitiative/openMINDS_Python/pull/XXXX + # by_name(..., ignore_accents=True) strips accents/diacritics (Unicode NFD) before matching + SovereignState = om.controlled_terms.SovereignState + + # (query, case_sensitive, ignore_accents, should match France) + cases = [ + ("République française", True, False, True), # exact + ("Republique francaise", True, True, True), # accents differ + ("république française", False, False, True), # case differs + ("republique francaise", False, True, True), # case and accents differ + ("republique francaise", True, False, False), # defaults: neither absorbed + ("Republique francaise", True, False, False), # accents still matter + ("république française", True, True, False), # case still matters + ] + for query, case_sensitive, ignore_accents, should_match in cases: + match = SovereignState.by_name(query, case_sensitive=case_sensitive, ignore_accents=ignore_accents) + assert (match is not None and match.name == "France") == should_match diff --git a/pipeline/translator.py b/pipeline/translator.py index 90847026..8c30deb1 100644 --- a/pipeline/translator.py +++ b/pipeline/translator.py @@ -248,6 +248,8 @@ def filter_instance(instance): "Real": "from numbers import Real", } extra_imports = set() + if has_instances: + extra_imports.add("import unicodedata") for property in self.context["properties"]: if isinstance(property["type"], list): for t in property["type"]: @@ -259,7 +261,9 @@ def filter_instance(instance): if imp: extra_imports.add(imp) if extra_imports: - self.context["preamble"] = "\n".join(sorted(extra_imports)) + stdlib_imports = sorted(i for i in extra_imports if not i.startswith("from openminds")) + openminds_imports = sorted(i for i in extra_imports if i.startswith("from openminds")) + self.context["preamble"] = "\n".join(stdlib_imports + openminds_imports) def build(self, embedded=None, class_to_module_map=None, class_full_modules=None): target_file_path = os.path.join("target", "openminds", f"{self._target_file_without_extension()}.py") From 09b33b997c39c1b993e43fae0c73df39daa9c76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Genevi=C3=A8ve=20Fleury?= <113036747+gefleury@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:44:02 +0200 Subject: [PATCH 2/4] Rename by_name() test to match the test_pr convention --- pipeline/tests/test_regressions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipeline/tests/test_regressions.py b/pipeline/tests/test_regressions.py index 62c28d45..7f0f3d05 100644 --- a/pipeline/tests/test_regressions.py +++ b/pipeline/tests/test_regressions.py @@ -731,8 +731,8 @@ def test_pr0100_by_name_match_within(om): @pytest.mark.parametrize("om", [openminds.latest]) -def test_prXXXX_by_name_ignore_accents(om): - # https://github.com/openMetadataInitiative/openMINDS_Python/pull/XXXX +def test_pr0103_by_name_ignore_accents(om): + # https://github.com/openMetadataInitiative/openMINDS_Python/pull/103 # by_name(..., ignore_accents=True) strips accents/diacritics (Unicode NFD) before matching SovereignState = om.controlled_terms.SovereignState From 3eed40f2c71bfb1bd0561b31846efe1e48961a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Genevi=C3=A8ve=20Fleury?= <113036747+gefleury@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:38:26 +0200 Subject: [PATCH 3/4] Move unicodedata import into remove_accents() --- pipeline/src/additional_methods/by_name.py.txt | 2 ++ pipeline/translator.py | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pipeline/src/additional_methods/by_name.py.txt b/pipeline/src/additional_methods/by_name.py.txt index 6fc1216c..dd3f9d76 100644 --- a/pipeline/src/additional_methods/by_name.py.txt +++ b/pipeline/src/additional_methods/by_name.py.txt @@ -47,6 +47,8 @@ cls._instance_lookup[key] = [instance] def remove_accents(s): + import unicodedata + nfd_form = unicodedata.normalize("NFD", s) return "".join(c for c in nfd_form if not unicodedata.combining(c)) diff --git a/pipeline/translator.py b/pipeline/translator.py index 8c30deb1..90847026 100644 --- a/pipeline/translator.py +++ b/pipeline/translator.py @@ -248,8 +248,6 @@ def filter_instance(instance): "Real": "from numbers import Real", } extra_imports = set() - if has_instances: - extra_imports.add("import unicodedata") for property in self.context["properties"]: if isinstance(property["type"], list): for t in property["type"]: @@ -261,9 +259,7 @@ def filter_instance(instance): if imp: extra_imports.add(imp) if extra_imports: - stdlib_imports = sorted(i for i in extra_imports if not i.startswith("from openminds")) - openminds_imports = sorted(i for i in extra_imports if i.startswith("from openminds")) - self.context["preamble"] = "\n".join(stdlib_imports + openminds_imports) + self.context["preamble"] = "\n".join(sorted(extra_imports)) def build(self, embedded=None, class_to_module_map=None, class_full_modules=None): target_file_path = os.path.join("target", "openminds", f"{self._target_file_without_extension()}.py") From 21e0397824bf70740751e4e11e3a323646a51bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Genevi=C3=A8ve=20Fleury?= <113036747+gefleury@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:58:44 +0200 Subject: [PATCH 4/4] Handle special letters in by_name() ignore_accents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove_accents() only stripped accents that Unicode can split off from their base letter (NFD normalization), so letters with no such decomposition (ß, œ, æ, ø, ł, đ, etc.) passed through unchanged. Add an explicit translation map, applied after the NFD strip so composed forms (e.g. "ǿ") are also handled correctly. Updated test. --- .../src/additional_methods/by_name.py.txt | 19 +++++++++++++++++-- pipeline/tests/test_regressions.py | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pipeline/src/additional_methods/by_name.py.txt b/pipeline/src/additional_methods/by_name.py.txt index dd3f9d76..5bbfef33 100644 --- a/pipeline/src/additional_methods/by_name.py.txt +++ b/pipeline/src/additional_methods/by_name.py.txt @@ -26,7 +26,9 @@ all (bool, optional): Whether to return all objects that match the name, or only the first. Defaults to False. case_sensitive (bool, optional): Whether the search should be case-sensitive. Defaults to True. ignore_accents (bool, optional): Whether to ignore accents (acute, grave, circumflex) and - other diacritical marks (cedilla, tilde, ring, etc.) when matching. Defaults to False. + other diacritical marks (cedilla, tilde, ring, etc.) when matching. Also treat + special letters (ß, œ, æ, ø, ł, etc.) as their closest plain-letter equivalents + (e.g. "ß" as "ss"). Defaults to False. """ namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation") if cls._instance_lookup is None: @@ -49,8 +51,21 @@ def remove_accents(s): import unicodedata + special = str.maketrans({ + "Ł": "L", "ł": "l", + "Ø": "O", "ø": "o", + "Đ": "D", "đ": "d", + "Ð": "D", "ð": "d", + "Þ": "Th", "þ": "th", + "Æ": "AE", "æ": "ae", + "Œ": "OE", "œ": "oe", + "ß": "ss", "ẞ": "SS", + "Ə": "E", "ə": "e", + "ı": "i", + }) nfd_form = unicodedata.normalize("NFD", s) - return "".join(c for c in nfd_form if not unicodedata.combining(c)) + stripped = "".join(c for c in nfd_form if not unicodedata.combining(c)) + return stripped.translate(special) def normalize(s): if not case_sensitive: diff --git a/pipeline/tests/test_regressions.py b/pipeline/tests/test_regressions.py index 7f0f3d05..b0481cf2 100644 --- a/pipeline/tests/test_regressions.py +++ b/pipeline/tests/test_regressions.py @@ -749,3 +749,17 @@ def test_pr0103_by_name_ignore_accents(om): for query, case_sensitive, ignore_accents, should_match in cases: match = SovereignState.by_name(query, case_sensitive=case_sensitive, ignore_accents=ignore_accents) assert (match is not None and match.name == "France") == should_match + + # ignore_accents also has to map special letters + special_letter_cases = [ + # (query, ignore_accents, expected_country_or_None) + ("Azərbaycan Respublikası", False, "Azerbaijan"), # exact + ("Azerbaycan Respublikasi", True, "Azerbaijan"), + ("Azerbaycan Respublikasi", False, None), + ("Wááshindoon Bikéyah Ałhidadiidzooígíí", False, "United States"), # exact + ("Waashindoon Bikeyah Alhidadiidzooigii", True, "United States"), + ("Waashindoon Bikeyah Alhidadiidzooigii", False, None), + ] + for query, ignore_accents, expected_name in special_letter_cases: + match = SovereignState.by_name(query, ignore_accents=ignore_accents) + assert (match.name if match else None) == expected_name