Source code for smufolib.utils.error

"""Utility functions for error message generation and type validation.

This module provides functions to generate and validate error messages,
check types, and suggest corrections for invalid values. It includes
a dictionary of error message templates to ensure streamlined and
consistent error reporting.

To import the module:

    >>> from smufolib import error

"""

from __future__ import annotations
from types import UnionType
from typing import get_args, Any
import difflib

from smufolib.utils._annotations import ErrorKey

# pylint: disable=C0103

#: Dictionary of error message templates.
ERROR_TEMPLATES: dict[str, str] = {
    "alphanumericValue": "The value for {objectName!r} must be alphanumeric",
    "alphanumericValueItems": "Value items for {objectName!r} must be alphanumeric",
    "argumentConflict": "The option '{key}' is already added as positional argument or flag",
    "attributeError": "{objectName!r} has no attribute {attribute!r}",
    "contextualAttributeError": "The attribute {attribute!r} is not available when {context}",
    "contextualSetAttributeError": "Cannot set attribute {attribute!r} when {context}",
    "contextualTypeError": "Expected {objectName!r} to be of type {validTypes} when {context}, but got {valueType}",
    "contextualItemsTypeError": "Items in {objectName!r} must be {validTypes} when {context}, not {valueType}",
    "deprecated": "{objectName!r} is deprecated and will be removed in the next version of SMufoLib (after {version})",
    "deprecatedReplacement": "Use {replacement!r} instead",
    "duplicateFlags": "Arguments {argument1!r} and {argument2!r} have duplicate short flag: {flag}",
    "duplicateAttributeValue": "The value {value!r} for {attribute!r} is already assigned to another {objectName} instance: {conflictingInstance!r}",
    "duplicateItems": "Items in {objectName!r} cannot be duplicates",
    "emptyValue": "The value for {objectName!r} cannot be empty",
    "emptyValueItems": "Value items for {objectName!r} cannot be empty",
    "fileNotFound": "The file or directory for {objectName!r} does not exist",
    "invalidFormat": "The value for {objectName!r} is not correctly formatted",
    "invalidInitialCharacter": "The value for {objectName!r} must start with a lowercase letter or number",
    "invalidInitialItemsCharacter": "Value items for {objectName!r} must start with a lowercase letter or number",
    "itemsTypeError": "Items in {objectName!r} must be {validTypes}, not {valueType}",
    "itemsValueError": "Invalid value for item in {objectName!r}: {value!r}",
    "missingDependency": "Cannot set {objectName!r} because {dependency!r} is None",
    "missingExtension": "The value for {objectName!r} must have a {extension!r} extension",
    "missingGlyph": "No SMuFL glyph named {name!r}",
    "missingValue": "Required values for {objectName!r} are missing",
    "nonIncreasingRange": "The values in {objectName!r} must form an increasing range",
    "notImplementedError": "The {objectName!r} subclass does not implement this method",
    "numericValue": "The value for {objectName!r} must be numeric",
    "overlappingRange": "Range '{name}' ({start}-{end}) overlaps with an existing range",
    "permissionError": "Permission denied: {context}",
    "recommendScript": "Consider running the script {scriptName!r} before the current process",
    "serializationError": "Error serializing JSON data or writing to the file",
    "singleItem": "{objectName!r} must contain a value pair",
    "suggestion": "Did you mean {suggestion!r}?",
    "typeError": "Expected {objectName!r} to be of type {validTypes}, but got {valueType}",
    "unicodeOutOfRange": "The value for {objectName!r} is outside the Unicode range {start}-{end}",
    "urlError": "Could not connect to URL: {url!r}",
    "valueError": "Invalid value for {objectName!r}: {value!r}",
    "valueTooHigh": "The value for {objectName!r} must be {value!r} or lower",
    "valueTooLow": "The value for {objectName!r} must be {value!r} or higher",
}


[docs] class URLWarning(Warning): """URL connection failure warning."""
[docs] def generateErrorMessage( *templateNames: ErrorKey, string: str | None = None, **kwargs ) -> str: r"""Generate an error message from a template and keyword arguments. The `templateNames` and `string` will be concatenated in order. :param \*templateNames: The error message template string, which should contain placeholders for keyword arguments. :param string: An additional message string to include. Defaults to :obj:`None` :param \**kwargs: Arbitrary keyword arguments corresponding to the placeholders in the template string. :raises KeyError: If a placeholder in the template does not have a corresponding keyword argument. Examples: >>> error.generateErrorMessage("alphanumericValue", objectName="unicode") "The value for 'unicode' must be alphanumeric" >>> error.generateErrorMessage( ... "typeError", objectName="index", validTypes="int", valueType="str" ... ) "Expected 'index' to be of type int, but got str" >>> error.generateErrorMessage( ... "urlError", string="Please try again", url="some/url.com" ... ) "Could not connect to URL: 'some/url.com'. Please try again" """ messages = [ERROR_TEMPLATES[n].format(**kwargs) for n in templateNames] if string: messages.append(string) return ". ".join(messages)
[docs] def generateTypeError( value: Any, validTypes: type | tuple[type, ...], objectName: str, context: str | None = None, items: bool = False, ) -> str: """Generate a :class:`TypeError` message. This function generates an error message based on the number of valid types. By default, the message is generated from :obj:`ERROR_TEMPLATES`:``"typeError"``. If `context` in not :obj:`None` (or empty) and `items` is :obj:`False`, :obj:`ERROR_TEMPLATES`:``"contextualTypeError"`` is used. If `items` is :obj:`True` and `context` is :obj:`None`, :obj:`ERROR_TEMPLATES`:``"itemsTypeError"`` is used. If both `context` is not :obj:`None` and `items` is :obj:`True`, :obj:`ERROR_TEMPLATES`:``"contextualItemsTypeError"`` is used. :param value: The value to be validated. :param validTypes: A :class:`tuple` or :class:`list` of valid types. :param objectName: The name of the object being validated. :param context: Additional substring for contextual type errors. Defaults to :obj:`None` :param items: Whether to use items-specific template. Defaults to :obj:`False`. :raises TypeError: If `value` does not match any of the valid types. :raises ValueError: If `items` is :obj:`True` and `value` is not an iterable. Examples: >>> error.generateTypeError(123, (str,), "path") "Expected 'path' to be of type str, but got int" >>> from pathlib import Path >>> error.generateTypeError(123, (str, Path), "path") "Expected 'path' to be of type str or Path, but got int" >>> from smufolib import Request >>> error.generateTypeError(123, (str, Path, Request), "path") "Expected 'path' to be of type str, Path or Request, but got int" """ typeNames = _listTypes(validTypes) valueType = type(value).__name__ kwargs = { "validTypes": typeNames, "objectName": objectName, "valueType": valueType, } if context: template: ErrorKey = ( "contextualItemsTypeError" if items else "contextualTypeError" ) kwargs["context"] = context elif items: template = "itemsTypeError" else: template = "typeError" return generateErrorMessage(template, **kwargs)
[docs] def validateType( value: Any, validTypes: type | tuple[type, ...] | UnionType, objectName: str, items=False, ) -> None: """Validate if a value matches any of the valid types. :param value: The value to be validated. :param validTypes: A single valid type or :class:`tuple` of valid types. :param objectName: The name of the object being validated. :param items: Whether to validate `value` items rather than `value` itself. Defaults to :obj:`False`. :raises TypeError: If `value` does not match any of the valid types. :raises ValueError: If `items` is :obj:`True` and any `value` item does not match any of the valid types. Examples: >>> try: ... error.validateType(123, str, "glyphName") ... except TypeError as e: ... print(e) Expected 'glyphName' to be of type str, but got int >>> myList = ["uniE000", 1] >>> for item in myList: ... try: ... error.validateType(item, str, "myList", items=True) ... except ValueError as e: ... print(e) Items in 'myList' must be str, not int """ if isinstance(validTypes, type): validTypes = (validTypes,) elif isinstance(validTypes, UnionType): validTypes = get_args(validTypes) if not isinstance(value, validTypes): if items: raise ValueError( generateTypeError(value, validTypes, objectName, items=items) ) else: raise TypeError(generateTypeError(value, validTypes, objectName))
[docs] def suggestValue( value: str, possibilities: list[str] | tuple[str, ...], objectName: str, cutoff: float = 0.6, items=False, ) -> str: """Validate value and suggests a valid close match. If `items` is :obj:`True`, an alternate error message template is used to validate the values of items within an :term:`iterable` value rather than the value itself. :param value: The value to be validated. :param possibilities: A list of valid possibilities for the value. :param objectName: The name of the object being validated. :param cutoff: A float value representing the cutoff threshold for similarity matches. Defaults to ``0.6``. :param items: Whether to use items-specific template. Defaults to :obj:`False`. :raises ValueError: If the provided value is not found in `possibilities` or if `items` is :obj:`True` and `value` is not an iterable. Example: >>> try: ... error.suggestValue( ... "spiltStemUpSE", ["splitStemUpSE", "splitStemUpSW"], ... "anchorName", cutoff=0.5) ... except ValueError as e: ... print(e) Invalid value for 'anchorName': 'spiltStemUpSE'. Did you mean 'splitStemUpSE'? """ if value in possibilities: return value closeMatches = difflib.get_close_matches(value, possibilities, 1, cutoff) if closeMatches: suggestion = closeMatches[0] template: ErrorKey = "itemsValueError" if items else "valueError" raise ValueError( generateErrorMessage( template, "suggestion", objectName=objectName, value=value, suggestion=suggestion, ) ) raise ValueError( generateErrorMessage("valueError", objectName=objectName, value=value) )
def _listTypes(types: type | tuple[type, ...] | UnionType) -> str: # Represent a series of object type names as a string. if isinstance(types, type): types = (types,) elif isinstance(types, UnionType): types = get_args(types) if len(types) > 1: typeNames = ( ", ".join(t.__name__ for t in types[:-1]) + f" or {types[-1].__name__}" ) else: typeNames = types[0].__name__ return typeNames