diff --git a/flexlibs2/code/Lexicon/AllomorphOperations.py b/flexlibs2/code/Lexicon/AllomorphOperations.py index 7ed5668..5180cb3 100644 --- a/flexlibs2/code/Lexicon/AllomorphOperations.py +++ b/flexlibs2/code/Lexicon/AllomorphOperations.py @@ -202,9 +202,11 @@ def Create(self, entry_or_hvo, form, morphType=None, wsHandle=None): Args: entry_or_hvo: The ILexEntry object or HVO. form (str): The allomorph form (e.g., "-ing", "walk", "pre-"). - morphType (IMoMorphType, optional): The morpheme type object. - If None (default), inherits from the entry's LexemeFormOA morph type, - matching FLEx GUI behavior. + morphType (IMoMorphType | str | None): The morpheme type. Pass an + IMoMorphType object, a name string (e.g. 'suffix', '=enclitic', + '-prefix'), or None to inherit from the entry's LexemeFormOA + morph type (matching FLEx GUI behaviour). Display markers are + stripped automatically so UI-copied names resolve correctly. wsHandle: Optional writing system handle. Defaults to vernacular WS. Returns: @@ -252,6 +254,30 @@ def Create(self, entry_or_hvo, form, morphType=None, wsHandle=None): entry = self.__GetEntryObject(entry_or_hvo) wsHandle = self.__WSHandle(wsHandle) + # Accept morph type as a string name (with or without display markers). + if isinstance(morphType, str): + bare = morphType.strip('-=~<>') + morph_types = self.project.lp.LexDbOA.MorphTypesOA + from ..Shared.string_utils import normalize_match_key, best_analysis_text + target = normalize_match_key(bare, casefold=True) + def _find(possibilities): + for mt in possibilities: + mt_name = best_analysis_text(mt.Name) + if mt_name and normalize_match_key(mt_name, casefold=True) == target: + return mt + if mt.SubPossibilitiesOS.Count > 0: + found = _find(mt.SubPossibilitiesOS) + if found: + return found + return None + resolved = _find(morph_types.PossibilitiesOS) if morph_types else None + if resolved is None: + raise FP_ParameterError( + f"Morph type '{morphType}' not found. " + "Use project.LexEntry.GetAvailableMorphTypes() to see valid names." + ) + morphType = resolved + # If no morphType provided, inherit from entry's LexemeFormOA (FLEx behavior) if morphType is None: if not entry.LexemeFormOA or not entry.LexemeFormOA.MorphTypeRA: diff --git a/flexlibs2/code/Lexicon/LexEntryOperations.py b/flexlibs2/code/Lexicon/LexEntryOperations.py index c1f3514..4fb297b 100644 --- a/flexlibs2/code/Lexicon/LexEntryOperations.py +++ b/flexlibs2/code/Lexicon/LexEntryOperations.py @@ -778,6 +778,8 @@ def GetLexemeForm(self, entry_or_hvo, wsHandle=None): Raises: FP_NullParameterError: If entry_or_hvo is None + FP_WritingSystemError: If wsHandle refers to a writing system not + configured in the project. Example: >>> entry = project.LexEntry.Find("run") @@ -876,6 +878,8 @@ def GetCitationForm(self, entry_or_hvo, wsHandle=None): Raises: FP_NullParameterError: If entry_or_hvo is None + FP_WritingSystemError: If wsHandle refers to a writing system not + configured in the project. Example: >>> entry = project.LexEntry.Find("run") @@ -1514,6 +1518,74 @@ def ValidateMorphType(self, morph_type_name): return (False, None, None) + @OperationsMethod + def GetAllByMorphType(self, name_or_list, match_allomorphs=False): + """ + Return all entries whose lexeme-form morph type matches the given name(s). + + Comparison is by GUID (not substring), so affix-family names like + 'suffix' never accidentally match 'circumfix-suffix' or similar. + Display markers are stripped before lookup, so '=enclitic' resolves the + same as 'enclitic'. + + Args: + name_or_list (str | list[str]): Morph type name(s) to match. A + single string matches one type; a list uses OR semantics (useful + for affix-family grouping such as + ['prefix', 'suffix', 'infix']). + match_allomorphs (bool): When False (default) only the lexeme-form + morph type is checked. When True, entries are also included if + any AlternateFormsOS allomorph matches. + + Returns: + list: ILexEntry objects whose morph type matches. + + Raises: + FP_ParameterError: If any name in name_or_list is not found in the + project's morph type list. + + Example: + >>> affixes = project.LexEntry.GetAllByMorphType(['prefix', 'suffix', 'infix']) + >>> stems = project.LexEntry.GetAllByMorphType('stem') + >>> enclitics = project.LexEntry.GetAllByMorphType('=enclitic') # markers OK + + Notes: + - An entry's morph type is on its lexeme form; allomorph morph types + may differ and are only checked when match_allomorphs=True. + - List input uses OR semantics; chain set() operations for AND/exclusion. + + See Also: + ValidateMorphType, GetMorphType, GetAvailableMorphTypes + """ + if isinstance(name_or_list, str): + name_or_list = [name_or_list] + + # Resolve all names to morph-type GUIDs up front so errors surface early. + target_guids = set() + for name in name_or_list: + mt = self.__FindMorphType(name) + if mt is None: + raise FP_ParameterError( + f"Morph type '{name}' not found. " + "Use GetAvailableMorphTypes() to see valid names." + ) + target_guids.add(mt.Guid) + + results = [] + for entry in self.project.lp.LexDbOA.Entries: + matched = False + if entry.LexemeFormOA and entry.LexemeFormOA.MorphTypeRA: + if entry.LexemeFormOA.MorphTypeRA.Guid in target_guids: + matched = True + if not matched and match_allomorphs: + for alm in entry.AlternateFormsOS: + if alm.MorphTypeRA and alm.MorphTypeRA.Guid in target_guids: + matched = True + break + if matched: + results.append(entry) + return results + # --- Sense Management --- @OperationsMethod @@ -2978,13 +3050,20 @@ def __FindMorphType(self, name): """ Find a morph type by name (case-insensitive). + Display markers (leading/trailing -, =, ~, <, >) are stripped before + matching so that UI-copied strings like '=enclitic' or '-suffix' resolve + to the same canonical type as 'enclitic' or 'suffix'. + Args: - name (str): The morph type name to search for + name (str): The morph type name to search for (bare or with display markers) Returns: IMoMorphType or None: The morph type object if found, None otherwise """ - target = normalize_match_key(name, casefold=True) + # Strip display markers (prefix '-', '=', '~'; postfix '-', '=', '~', '<', '>') + # that FLEx shows in the UI but that are NOT part of the canonical IMoMorphType.Name. + bare = name.strip('-=~<>') + target = normalize_match_key(bare, casefold=True) wsHandle = self.project.project.DefaultAnalWs morph_types = self.project.lp.LexDbOA.MorphTypesOA diff --git a/flexlibs2/code/Lexicon/LexSenseOperations.py b/flexlibs2/code/Lexicon/LexSenseOperations.py index 38851ed..8ed2b86 100644 --- a/flexlibs2/code/Lexicon/LexSenseOperations.py +++ b/flexlibs2/code/Lexicon/LexSenseOperations.py @@ -792,6 +792,9 @@ def GetGloss(self, sense_or_hvo, wsHandle=None): Raises: FP_NullParameterError: If sense_or_hvo is None. + FP_WritingSystemError: If wsHandle refers to a writing system not + configured in the project. Callers iterating over writing + systems should catch this or verify with project.WSHandle first. Example: >>> entry = list(project.LexiconAllEntries())[0] @@ -884,6 +887,8 @@ def GetDefinition(self, sense_or_hvo, wsHandle=None): Raises: FP_NullParameterError: If sense_or_hvo is None. + FP_WritingSystemError: If wsHandle refers to a writing system not + configured in the project. Example: >>> entry = list(project.LexiconAllEntries())[0]