Source code for rics.translation.offline._format

import re
from dataclasses import dataclass
from typing import Iterable, List, Tuple

from rics.translation.offline.types import FormatType, PlaceholdersTuple
from rics.utility.misc import tname

_REQUIRED_ELEMENT_RE = r"{(?P<required>\w+)}"
_OPTIONAL_ELEMENT_REGEX = r"(?P<optional>\[(?P<left>.*?){(?P<optional_name>\w+)}(?P<right>.*?)\])"


[docs]class Format: """Format specification for translations strings. Translation formats are similar to regular f-strings, with two important exceptions: 1. Positional arguments (``'{}'``) may not be used; correct form is ``'{key-name}'``. 2. Substrings surrounded by ``[]`` denote an optional element. Args: fmt: A translation fstring. Examples: A format string with an optionl element. >>> from rics.translation.offline import Format >>> fmt = Format('{id}:{name}[, nice={is_nice}]') The ``Format`` class when used directly only returns required placeholders by default.. >>> fmt.fstring(), fmt.fstring().format(id=0, name='Tarzan') ('{id}:{name}', '0:Tarzan') ..but the `placeholders` attribute can be used to retrieve all placeholders, required and optional: >>> fmt.placeholders ('id', 'name', 'is_nice') >>> fmt.fstring(fmt.placeholders), fmt.fstring(fmt.placeholders).format(id=1, name='Morris', is_nice=True) ('{id}:{name}, nice={is_nice}', '1:Morris, nice=True') """ PLACEHOLDER_PATTERN: re.Pattern = re.compile(_OPTIONAL_ELEMENT_REGEX + "|" + _REQUIRED_ELEMENT_RE) """Pattern which denotes placeholder elements in format strings.""" def __init__(self, fmt: str) -> None: self._fmt = fmt self._elements = self._parse_format_string(fmt) self._named_elements: List[NamedElement] = [] for elem in self._elements: if isinstance(elem, NamedElement): self._named_elements.append(elem)
[docs] def fstring(self, placeholders: Iterable[str] = None, positional: bool = False) -> str: """Create a format string for the given placeholders. Args: placeholders: Keys to keep. Passing ``None`` is equivalent to passing :attr:`required_placeholders`. positional: If ``True``, remove names to return a positional fstring. Returns: An fstring with optional elements removed unless included in `placeholders`. Raises: KeyError: If required placeholders are missing. """ placeholders = placeholders or self.required_placeholders missing_required_placeholders = set(self.required_placeholders).difference(placeholders) if missing_required_placeholders: raise KeyError(f"Required key(s) {missing_required_placeholders} missing from {placeholders=}.") return self._make_fstring(placeholders, positional=positional)
def _make_fstring(self, placeholders: Iterable[str], positional: bool) -> str: parts = [] for element in self._elements: if isinstance(element, NamedElement): if element.name in placeholders: parts.append(element.positional_part if positional else element.part) else: parts.append(element.part) return "".join(parts)
[docs] @staticmethod def parse(fmt: FormatType) -> "Format": """Parse a format. Args: fmt: Input to parse. Returns: A ``Format`` instance. """ return fmt if isinstance(fmt, Format) else Format(fmt)
@property def placeholders(self) -> PlaceholdersTuple: """All placeholders in the order in which they appear.""" return tuple(e.name for e in self._named_elements) @property def required_placeholders(self) -> PlaceholdersTuple: """All required placeholders in the order in which they appear.""" return tuple(e.name for e in filter(lambda e: e.required, self._named_elements)) @property def optional_placeholders(self) -> PlaceholdersTuple: # pragma: no cover """All optional placeholders in the order in which they appear.""" return tuple(e.name for e in filter(lambda e: not e.required, self._named_elements)) @classmethod def _parse_format_string(cls, format_string: str) -> Tuple["Element", ...]: """Parse a translation format string. Args: format_string: A format string to parse. Returns: A tuple of elements. """ ans = [] pos = 0 while True: match = Format.PLACEHOLDER_PATTERN.search(format_string, pos=pos) if match is None: break else: if match.start() > pos: ans.append(Element(format_string[pos : match.start()], True)) ans.append(from_match(match)) pos = match.end() if pos < len(format_string): ans.append(Element(format_string[pos:], True)) return tuple(ans) def __repr__(self) -> str: return f"{tname(self)}{tuple(e.part for e in self._elements)}"
_POSITIONAL_PATTERN: re.Pattern = re.compile(_REQUIRED_ELEMENT_RE) @dataclass(frozen=True) class Element: """A single translation element.""" part: str required: bool @property def positional_part(self) -> str: """Return a positional copy of `part`.""" return _POSITIONAL_PATTERN.sub("{}", self.part, 1) @dataclass(frozen=True) class NamedElement(Element): """A single translation element.""" name: str def from_match(match: re.Match) -> NamedElement: """Initialize an element from a RegEx match instance. Args: match: RegEx match instance. Returns: A new element. """ part, required, key = ( ( match.group(0), True, match.group("required"), ) if match.group("optional") is None else ( # fmt: off match.group("left") + "{" + match.group("optional_name") + "}" + match.group("right"), False, match.group("optional_name"), ) ) return NamedElement(part, required, key)