"""Utilities for normalizing various data types in SMufoLib.
This module extends :mod:`fontParts.base.normalizers` and provides
functions to normalize objects and data related to SMuFL fonts and
metadata requests. It includes normalization for:
- :class:`.Font` objects
- :class:`.Smufl` objects and attributes
- :class:`.EngravingDefaults` objects and attributes
- :class:`.Request` objects and attributes
These functions ensure that the data adheres to expected formats and
values, handling type validation and normalization as needed.
In addition to the documented functions below, it provides direct access
to the following external normalizers for convenience:
- :func:`~fontParts.base.normalizers.normalizeBoolean`
- :func:`~fontParts.base.normalizers.normalizeColor`
- :func:`~fontParts.base.normalizers.normalizeVisualRounding`
- :func:`~fontParts.base.normalizers.normalizeGlyph`
To import the module:
>>> from smufolib import normalizers
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from pathlib import Path
# ruff: noqa: F401
# pylint: disable=W0611
from fontParts.base.normalizers import normalizeInternalObjectType
from fontParts.base.normalizers import normalizeBoolean
from fontParts.base.normalizers import normalizeColor
from fontParts.base.normalizers import normalizeVisualRounding
from fontParts.base.normalizers import normalizeGlyph
from smufolib.utils import error, converters
from smufolib.utils._annotations import (
EngravingDefaultsInput,
EngravingDefaultsReturn,
CollectionType,
PairCollectionType,
PairType,
ErrorKey,
)
# ruff: noqa: F401
# pylint: enable=W0611
if TYPE_CHECKING: # pragma: no cover
from smufolib.objects.font import Font
from smufolib.request import Request
from smufolib.objects.smufl import Smufl
from smufolib.objects.engravingDefaults import EngravingDefaults
# pylint: disable=C0103, C0415
# ----
# Font
# ----
[docs]
def normalizeFont(value: Font) -> Font:
"""Normalize Font object.
:param value: The value to normalize.
:raises TypeError: If `value` is not the accepted type.
"""
from smufolib.objects.font import Font
return normalizeInternalObjectType(value, Font, "Font")
# -----
# Smufl
# -----
[docs]
def normalizeClasses(value: CollectionType[str] | None) -> tuple[str, ...]:
"""Normalize smufl classes.
:param value: The value to normalize.
:raises TypeError: If `value` is not an accepted type.
:raises ValueError:
- If any `value` item does not normalize
with :func:`normalizeSmuflName`.
- If any `value` items are duplicates.
"""
if value is None:
return ()
objectName = "Smufl.classes"
error.validateType(value, (tuple, list), objectName)
for val in value:
error.validateType(val, str, objectName, items=True)
for v in val.split("_"):
normalizeSmuflName(v, "Smufl.classes", items=True)
duplicates = {v for v in value if value.count(v) > 1}
if len(duplicates) != 0:
raise ValueError(
error.generateErrorMessage("duplicateItems", objectName=objectName)
)
return tuple(v for v in value if v)
[docs]
def normalizeDescription(value: str | None, attributeName: str) -> str | None:
"""Normalize human-readable smufl descriptions.
:param value: The value to normalize.
:param attributeName: The name of the attribute to normalize.
:raises TypeError: If `value` is not an accepted type.
:raises ValueError: If `value` is an empty string.
"""
if value is None:
return None
error.validateType(value, (str, type(None)), attributeName)
if len(value) == 0:
raise ValueError(
error.generateErrorMessage("emptyValue", objectName=attributeName)
)
return value
[docs]
def normalizeDesignSize(value: int | None) -> int | None:
"""Normalize design size.
:param value: The value to normalize.
:raises TypeError: If `value` is not an accepted type.
:raises ValueError: If `value` is less than ``10``.
"""
error.validateType(value, (int, type(None)), "Smufl.designSize")
if value is None:
return None
if value < 10:
raise ValueError(
error.generateErrorMessage(
"valueTooLow", objectName="Smufl.designSize", value=10
)
)
return value
[docs]
def normalizeSizeRange(value: PairCollectionType[int] | None) -> PairType[int] | None:
"""Normalize design size.
:param value: The value to normalize.
:raises TypeError: If `value` or it's contents is not an accepted
type.
:raises ValueError:
- If `value` does not contain a value pair.
- If `value` is not an increasing range.
- If `value` items do not normalize
with :func:`normalizeDesignSize`, except not be :obj:`None`.
"""
if value is None:
return None
objectName = "Smufl.sizeRange"
error.validateType(value, (tuple, list, type(None)), objectName)
if len(value) != 2:
raise ValueError(
error.generateErrorMessage("singleItem", objectName="Smufl.sizeRange")
)
start, end = value
startNormalized = normalizeDesignSize(start)
endNormalized = normalizeDesignSize(end)
if start >= end:
raise ValueError(
error.generateErrorMessage("nonIncreasingRange", objectName=objectName)
)
return startNormalized, endNormalized # type: ignore
[docs]
def normalizeSmufl(value: Smufl) -> Smufl:
"""Normalize Smufl object.
:param value: The value to normalize.
:raises TypeError: If `value` is not the accepted type.
"""
from smufolib.objects.smufl import Smufl
return normalizeInternalObjectType(value, Smufl, "Smufl")
[docs]
def normalizeSmuflName(
value: str | None, attributeName: str, items: bool = False
) -> str | None:
"""Normalize cannonical SMuFL names of glyphs, classes and ranges.
:param value: The value to normalize.
:param attributeName: The name of the attribute to normalize.
:param items: Whether to normalize `value` items rather than `value`
itself. Defaults to :obj:`False`.
:raises TypeError: If `value` is not an accepted type.
:raises ValueError:
- If `value` is an empty string.
- If `value` is value contains non-alphanumeric characters.
- If `value` does not start with a lowercase letter or number.
"""
if value is None:
return None
error.validateType(value, (str, type(None)), attributeName)
if not value:
template: ErrorKey = "emptyValueItems" if items else "emptyValue"
raise ValueError(error.generateErrorMessage(template, objectName=attributeName))
for val in value:
if not val.isalnum():
template = "alphanumericValueItems" if items else "alphanumericValue"
raise ValueError(
error.generateErrorMessage(template, objectName=attributeName)
)
if value[0].isupper():
template = (
"invalidInitialItemsCharacter" if items else "invalidInitialCharacter"
)
raise ValueError(error.generateErrorMessage(template, objectName=attributeName))
return value
# ------------------
# Engraving defaults
# ------------------
[docs]
def normalizeEngravingDefaults(value: EngravingDefaults) -> EngravingDefaults:
"""Normalize EngravingDefaults object.
:param value: The value to normalize.
:raises TypeError: If `value` is not the accepted type.
"""
from smufolib.objects.engravingDefaults import EngravingDefaults
return normalizeInternalObjectType(value, EngravingDefaults, "EngravingDefaults")
[docs]
def normalizeEngravingDefaultsAttr(
name: str, value: EngravingDefaultsInput | None
) -> EngravingDefaultsReturn | None:
"""Normalize engraving defaults attribute.
:param name: The attribute name to normalize.
:param value: The attribute value to normalize.
:raises TypeError: If any parameter value is not an accepted type.
:raises AttributeError: If name is not a
valid :class:`.EngravingDefaults` attribute name.
:raises ValueError: If ``"name=textFontFamily"`` and `value` is not
a :class:`str` or empty.
"""
from smufolib.objects.engravingDefaults import ENGRAVING_DEFAULTS_ATTRIBUTES
if not isinstance(name, str):
raise TypeError(
error.generateTypeError(value=name, validTypes=str, objectName="name")
)
className = "EngravingDefaults"
if name not in ENGRAVING_DEFAULTS_ATTRIBUTES:
raise AttributeError(
error.generateErrorMessage(
"attributeError", objectName=className, attribute=name
)
)
if value is None:
return () if name == "textFontFamily" else None
objectName = f"{className}.{name}"
if name == "textFontFamily":
if not isinstance(value, (tuple, list)):
raise TypeError(
error.generateTypeError(
value=value, validTypes=(tuple, list), objectName=objectName
)
)
value = _normalizeStringTuple(objectName, value)
elif not isinstance(value, (int, float)):
raise TypeError(
error.generateTypeError(
value=value, validTypes=(int, float), objectName=objectName
)
)
return value
def _normalizeStringTuple(
objectName: str, value: CollectionType[str]
) -> tuple[str, ...]:
# Normalize string tuple.
error.validateType(value, (tuple, list), objectName)
for val in value:
error.validateType(val, str, objectName, items=True)
if not val:
raise ValueError(
error.generateErrorMessage("emptyValueItems", objectName=objectName)
)
return tuple(value)
# -------
# Request
# -------
[docs]
def normalizeRequest(value: Request) -> Request:
"""Normalize Request object.
:param value: The value to normalize.
:raises TypeError: If `value` is not the accepted type.
"""
from smufolib.request import Request
return normalizeInternalObjectType(value, Request, "Request")
[docs]
def normalizeRequestPath(value: Path | str | None, parameter: str) -> str | None:
"""Normalize Request path.
Relative paths are resolved automatically.
:param value: The value to normalize.
:param parameter: The name of the parameter being validated.
:raises TypeError: If `value` is not the accepted type.
"""
if value is None:
return None
error.validateType(value, (str, Path), parameter)
if str(value).startswith("."):
return str(Path(value).resolve())
return str(value)