Source code for flexicon.code.Shared.string_utils
# -*- coding: utf-8 -*-
#
# flexlibs2.code.Shared.string_utils
#
# String utility functions for FlexLibs2.
#
# 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 flexlibs2.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 flexlibs2.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 flexlibs2.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)