Source code for flexicon.code.Shared.string_utils

# -*- coding: utf-8 -*-
#
#   flexicon.code.Shared.string_utils
#
#   String utility functions for Flexicon.
#
#   FLEx/LCM uses '***' as a placeholder when multilingual string fields
#   have no value set. This module provides utilities to normalize these
#   values to empty strings, matching the behavior of FlexLibs stable.
#

import unicodedata

# FLEx's null marker for empty multilingual string fields
FLEX_NULL_MARKER = "***"


[docs] def normalize_text(text): """ Normalize a text value from LCM, converting FLEx's null marker to empty string. FLEx/LCM uses ``***`` as a placeholder when multilingual string fields (IMultiString, IMultiUnicode) have no value set. This function normalizes such values to empty strings for consistent handling. Args: text: A string value from an LCM text field, or None Returns: The original text if it has content, or "" if None/empty/``***`` Example: >>> from flexicon.code.Shared.string_utils import normalize_text >>> normalize_text("***") '' >>> normalize_text(None) '' >>> normalize_text("hello") 'hello' >>> normalize_text("") '' """ if text is None: return "" if text == FLEX_NULL_MARKER: return "" return text
[docs] def normalize_match_key(text, casefold=True): """ Produce a key suitable for matching a Python-side string against an FLEx-stored multilingual string value. FLEx stores all IMultiString/IMultiUnicode values in NFD; Python source is typically NFC. Without normalization, any string containing combined diacritics will silently fail to match. Apply this to BOTH sides of any Find/Exists comparison. Args: text: Input string (may be None, '', or ``***``). casefold: If True (default), apply str.casefold() after NFD normalization. Pass False for case-sensitive Find methods. Use casefold (not lower) for correct handling of Turkish dotted/dotless I, German ess-zett, etc. Returns: Normalized string. Empty string for None/empty/``***`` inputs. Example: >>> needle = normalize_match_key("oo", casefold=False) >>> haystack = normalize_match_key(ITsString(p.Name.get_String(ws)).Text, ... casefold=False) >>> needle == haystack # True even when Python NFC differs from LCM NFD True """ text = normalize_text(text) if not text: return "" text = unicodedata.normalize("NFD", text) if casefold: text = text.casefold() return text
[docs] def normalize_ws_handle(ws): """ Normalize a writing-system argument to an integer handle. LCM methods such as ``ITsMultiString.get_String`` require a plain ``int`` handle. Users naturally obtain writing-system objects from ``project.WritingSystems`` (``CoreWritingSystemDefinition``) or pass an int they already have. This helper smooths over both cases so wrappers don't expose a confusing pythonnet ``TypeError`` when the caller passes an object instead of a raw handle. Args: ws: An ``int`` handle, a ``CoreWritingSystemDefinition`` (or any object that exposes a ``.Handle`` attribute returning an ``int``), or ``None``. Returns: ``int`` handle, or ``None`` if ``ws`` is ``None``. Raises: TypeError: If ``ws`` is not ``None``, not an ``int``, and has no ``.Handle`` attribute. Example: >>> from flexicon.code.Shared.string_utils import normalize_ws_handle >>> normalize_ws_handle(123) 123 >>> normalize_ws_handle(None) is None True >>> # CoreWritingSystemDefinition object with .Handle == 1 >>> normalize_ws_handle(ws_def) 1 """ if ws is None: return None if isinstance(ws, int): return ws handle = getattr(ws, "Handle", None) if handle is not None: return int(handle) raise TypeError( f"Unsupported writing-system argument type: {type(ws).__name__}. " "Pass an int handle or a CoreWritingSystemDefinition object." )
[docs] def is_empty_text(text): """ Check if a text value from LCM is empty (None, empty string, or ``***``). Args: text: A string value from an LCM text field, or None Returns: True if the text represents an empty/unset value Example: >>> from flexicon.code.Shared.string_utils import is_empty_text >>> is_empty_text("***") True >>> is_empty_text(None) True >>> is_empty_text("") True >>> is_empty_text("hello") False """ if text is None: return True if not text or text == FLEX_NULL_MARKER: return True return False
[docs] def best_analysis_text(multi_obj): """ Get the best analysis alternative text from an IMultiString/IMultiUnicode, normalized to empty string if unset. This combines accessing .BestAnalysisAlternative.Text with null marker handling. Args: multi_obj: An IMultiString or IMultiUnicode object, or None Returns: The text content, or "" if None/empty/``***`` Example: >>> text = best_analysis_text(sense.Definition) >>> text = best_analysis_text(pos.Name) """ if multi_obj is None: return "" text = multi_obj.BestAnalysisAlternative.Text return normalize_text(text)
[docs] def best_vernacular_text(multi_obj): """ Get the best vernacular alternative text from an IMultiString/IMultiUnicode, normalized to empty string if unset. This combines accessing .BestVernacularAlternative.Text with null marker handling. Args: multi_obj: An IMultiString or IMultiUnicode object, or None Returns: The text content, or "" if None/empty/``***`` Example: >>> text = best_vernacular_text(entry.LexemeFormOA.Form) """ if multi_obj is None: return "" text = multi_obj.BestVernacularAlternative.Text return normalize_text(text)
[docs] def best_text(multi_obj): """ Get the best analysis-or-vernacular alternative text from an IMultiString/IMultiUnicode, normalized to empty string if unset. This combines accessing .BestAnalysisVernacularAlternative.Text with null marker handling. Prefers analysis writing system, falls back to vernacular. Args: multi_obj: An IMultiString or IMultiUnicode object, or None Returns: The text content, or "" if None/empty/``***`` Example: >>> text = best_text(sense.Gloss) """ if multi_obj is None: return "" text = multi_obj.BestAnalysisVernacularAlternative.Text return normalize_text(text)
[docs] def best_multistring_alternative(mua, default_anal_ws, default_vern_ws, fallback_anal_ws_handles=(), fallback_vern_ws_handles=()): """ Get the best-available alternative from a bare ``ITsMultiString`` as an ``ITsString``, without relying on ``BestAnalysisVernacularAlternative``. ``ITsMultiString`` (``SIL.LCModel.Core.KernelInterfaces``) does NOT implement ``IMultiAccessorBase``, so it has no ``BestAnalysisAlternative`` / ``BestVernacularAlternative`` / ``BestAnalysisVernacularAlternative`` accessors -- only ``get_String(wsHandle)``, ``StringCount``, and ``GetStringFromIndex(i, out ws)``. This helper reimplements the "best alternative" priority walk directly on top of ``get_String`` so callers that only hold an ``ITsMultiString`` (e.g. custom field values obtained via ``get_MultiStringProp``) can still get a sensible fallback string. Priority order (first hit wins). A candidate is a MISS if it is None or its ``.Text`` is falsy or equal to the FLEx null marker (``***``): 1. ``default_anal_ws`` 2. ``default_vern_ws`` 3. each handle in ``fallback_anal_ws_handles``, in order 4. each handle in ``fallback_vern_ws_handles``, in order Args: mua: An ``ITsMultiString`` (or ``None``). default_anal_ws: Default analysis writing-system handle (int). default_vern_ws: Default vernacular writing-system handle (int). fallback_anal_ws_handles: Ordered iterable of additional analysis writing-system handles to try (int handles). fallback_vern_ws_handles: Ordered iterable of additional vernacular writing-system handles to try (int handles). Returns: The first non-empty alternative as an ``ITsString``, or ``None`` if every candidate was empty/unset or ``mua`` is ``None``. Callers must handle the ``None`` case (e.g. falling back to an empty ``ITsString``) since this helper never fabricates one itself. Example: >>> best = best_multistring_alternative( ... mua, project.DefaultAnalWs, project.DefaultVernWs, ... anal_handles, vern_handles) >>> if best is None: ... best = ITsString(mua.get_String(project.DefaultAnalWs)) """ if mua is None: return None def _hit(ws_handle): if ws_handle is None: return None candidate = mua.get_String(ws_handle) if candidate is None: return None text = getattr(candidate, "Text", None) if not text or text == FLEX_NULL_MARKER: return None return candidate result = _hit(default_anal_ws) if result is not None: return result result = _hit(default_vern_ws) if result is not None: return result for ws_handle in fallback_anal_ws_handles: result = _hit(ws_handle) if result is not None: return result for ws_handle in fallback_vern_ws_handles: result = _hit(ws_handle) if result is not None: return result return None