Source code for smufolib.objects.range

# pylint: disable=C0114
from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, Type
from collections.abc import Iterator

from smufolib.request import Request
from smufolib import config
from smufolib.utils import converters, error, normalizers
from smufolib.objects import _lib

if TYPE_CHECKING:  # pragma: no cover
    from smufolib.objects.smufl import Smufl
    from smufolib.objects.glyph import Glyph
    from smufolib.objects.font import Font
    from smufolib.objects.layer import Layer

CONFIG = config.load()
EDITABLE = editable = CONFIG["ranges"]["editable"]
METADATA = Request.ranges()
RANGES_LIB_KEY = "com.smufolib.ranges"
NAMES_LIB_KEY = "com.smufolib.names"

RangeValue = str | int | tuple["Glyph", ...] | None
T = TypeVar("T", bound=RangeValue)


[docs] class Range: """SMuFL range-related metadata. This object provides access to metadata describing how a :class:`.Glyph` relates to SMuFL-defined glyph ranges. Specified range data is sourced from :confval:`metadata.paths.ranges`, falling back to :confval:`metadata.fallbacks.ranges` if the former is unavailable. Like :class:`.Font` or :class:`.Layer`, this object behaves like a collection of :class:`.Glyph` instances. However, membership checks are based on :attr:`.Smufl.name` rather than :attr:`Glyph.name <fontParts.base.BaseGlyph.name>`. Ranges are read-only by default. Editability is controlled globally via the :confval:`ranges.editable` configuration setting. New custom ranges may be added using :meth:`.Smufl.newRange`. This data will be stored in the font's :class:`Lib <fontParts.base.BaseLib>` object. .. versionadded:: 0.7.0 Support for optional editability. :param smufl: The range's parent :class:`.Smufl` object. This object is typically accessed through a glyph's Smufl metadata interface: >>> glyph = font["uniE050"] >>> range = glyph.smufl.ranges[0] You may also instantiate it independently and assign it to a glyph later: >>> range = Range() # doctest: +SKIP """ # pylint: disable=invalid-name def __init__(self, smufl: Smufl | None = None, _internal: bool = True) -> None: self._smufl = smufl self._internal = _internal def __repr__(self) -> str: return ( f"<{self.__class__.__name__} {self.name!r} " f"({self.strStart}-{self.strEnd}) editable={EDITABLE} at {id(self)}>" ) def __bool__(self) -> bool: """Check if the range is valid. A range is considered valid if it has a name, a start, and an end Unicode value. """ return self.name is not None and self.start is not None and self.end is not None def __eq__(self, other: object) -> bool: """Check if two ranges are equal. Two ranges are considered equal if they have the same name, start, and end Unicode values. :param other: The other object to compare with. """ return ( isinstance(other, Range) and self.name == other.name and self.start == other.start and self.end == other.end ) def __hash__(self) -> int: return hash((self.name, self.start, self.end)) # ----------------- # Glyph Interaction # ----------------- def __contains__(self, name: str) -> bool: """Check if a glyph name is part of the range. :param name: The :attr:`.Smufl.name` of the glyph to check. """ glyphNames = self._getAttribute("glyphNames", tuple) return name in glyphNames if glyphNames else False def __getitem__(self, name: str) -> Glyph: """Get a SMuFL glyph by its canonical name from the range. :param name: The :attr:`.Smufl.name` of the glyph to retrieve. :raises TypeError: If `name` or `glyph` is not of the expected type. :raises ValueError: If `name` is not a valid SMuFL name. :raises KeyError: If the glyph is not found. Example: >>> glyph = range["accidentalFlat"] # doctest: +ELLIPSIS <Glyph 'uniE260' ['accidentalFlat'] ('public.default') at ...> """ if self._smufl is not None: glyph = self._smufl[name] if ( glyph is not None and glyph.unicode is not None and self.start <= glyph.unicode <= self.end ): return glyph raise KeyError(error.generateErrorMessage("missingGlyph", name=name)) def __setitem__(self, name: str, glyph: Glyph) -> None: """Insert or replace a SMuFL glyph in the range. If `glyph` is considered recommended (i.e., listed in :confval:`metadata.glyphnames`), it will be assigned a corresponding :attr:`~fontParts.base.BaseGlyph.name` and :attr:`~fontParts.base.BaseGlyph.unicode`. If it is optional or not a SMUFL glyph, `name` will be used if ``glyph.name`` is :obj:`None`. :param name: The :attr:`.Smufl.name` of the glyph to insert or replace. :param glyph: The :class:`.Glyph` object to insert. :raises TypeError: If `name` or `glyph` is not of the expected type. :raises ValueError: - If `name` is not a valid SMuFL name. - If `glyph` is not within the range's Unicode range. Example: >>> range["accidentalFlat"] = glyph """ if self._smufl is None or self.font is None: return self._smufl[name] = glyph inserted = self._smufl[name] if ( inserted is None or inserted.unicode is None or not self.start <= inserted.unicode <= self.end ): del self[name] raise ValueError( error.generateErrorMessage( "unicodeOutOfRange", objectName=name, start=self.strStart, end=self.strEnd, ) ) def __delitem__(self, name: str) -> None: """Delete a SMuFL glyph from the range. :param name: The :attr:`.Smufl.name` of the glyph to delete. :raises KeyError: If the glyph does not exist in the range. Example: >>> del range["accidentalFlat"] """ if self._smufl is not None: del self._smufl[name] def __iter__(self) -> Iterator[Glyph]: return iter(self.glyphs or ()) def __len__(self) -> int: return len(self.glyphs) if self.glyphs is not None else 0
[docs] def keys(self): """Return a view of the canonical SMuFL glyph names in the range.""" names = _lib.getLibSubdict(self.font, NAMES_LIB_KEY) filtered = {k: v for k, v in names.items() if k in self} return filtered.keys()
# ------- # Parents # ------- @property def smufl(self) -> Smufl | None: """Parent :class:`.Smufl` object. Example: >>> range.smufl # doctest: +ELLIPSIS <Smufl in glyph 'uniE050' ['gClef'] ('public.default') at ...> """ return self._smufl @smufl.setter def smufl(self, value: Smufl | None) -> None: if value is not None: self._smufl = normalizers.normalizeSmufl(value) @property def glyph(self) -> Glyph | None: """Parent :class:`.Glyph` object. Example: >>> range.glyph # doctest: +ELLIPSIS <Glyph 'uniE050' ['gClef'] ('public.default') at ...> """ if self._smufl is None: return None return self._smufl.glyph @property def font(self) -> Font | None: """Parent :class:`.Font` object. Example: >>> range.font # doctest: +ELLIPSIS <Font 'MyFont Regular' path='/path/to/MyFont.ufo' at ...> """ if self._smufl is None: return None return self._smufl.font @property def layer(self) -> Layer | None: """Parent :class:`.Layer` object. Example: >>> range.layer # doctest: +ELLIPSIS <Layer 'public.default' at ...> """ if self._smufl is None: return None return self._smufl.layer # ---- # Data # ---- @property def name(self) -> str | None: """Unique identifier of the glyph's affiliated SMuFL range. Example: >>> range.name 'clefs' """ return self._getAttribute("identifier", str) @property def description(self) -> str | None: """Human-readable description of the glyph's affiliated SMuFL range. Example: >>> range.description 'Clefs' """ return self._getAttribute("description", str) @property def glyphs(self) -> tuple[Glyph, ...] | None: """:class:`.Glyph` objects of the glyph's affiliated SMuFL range. Example: >>> range.glyphs # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE (<Glyph 'uniE050' ['gClef'] ('public.default') at ...>, ... <Glyph 'uniE07F' ['clefChangeCombining'] ('public.default') at ...>) """ return self._getAttribute("glyphs", tuple) @property def start(self) -> int | None: """Start unicode of the glyph's affiliated SMuFL range. Example: >>> range.start 57424 """ return self._getAttribute("range_start", int) @property def end(self) -> int | None: """End unicode of the glyph's affiliated SMuFL range. Example: >>> range.end 57471 """ return self._getAttribute("range_end", int) @property def strStart(self) -> str | None: """Start of the glyph's affiliated SMuFL range as Unicode string. Example: >>> range.strStart 'U+E050' """ return self._getAttribute("range_start", str) @property def strEnd(self) -> str | None: """End of the glyph's affiliated SMuFL range as Unicode string. Example: >>> range.strEnd 'U+E07F' """ return self._getAttribute("range_end", str) def _getAttribute(self, key: str, expectedType: Type[T]) -> T | None: # Get metadata attributes from ranges.json. data = ( _lib.getLibSubdict(self.font, RANGES_LIB_KEY) if self._internal else METADATA ) if self.glyph is None or data is None or isinstance(data, str): return None for range_, attributes in data.items(): if self._smufl is None or ( not self._internal and self._smufl.name not in attributes["glyphs"] ): continue value = attributes.get(key) if key == "identifier": value = range_ if key == "glyphNames": value = tuple(attributes["glyphs"]) if key == "glyphs": value = tuple( self._smufl[n] for n in attributes[key] if n in self._smufl and self._smufl[n] is not None ) if key in {"range_start", "range_end"}: value = attributes.get(key) if expectedType is int and isinstance(value, str): value = converters.toDecimal(value) elif expectedType is str and isinstance(value, int): value = converters.toUniHex(value) if isinstance(value, expectedType): return value return None