Spaces:
Sleeping
Sleeping
| """ | |
| Code of the config system; not related to fontTools or fonts in particular. | |
| The options that are specific to fontTools are in :mod:`fontTools.config`. | |
| To create your own config system, you need to create an instance of | |
| :class:`Options`, and a subclass of :class:`AbstractConfig` with its | |
| ``options`` class variable set to your instance of Options. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from dataclasses import dataclass | |
| from typing import ( | |
| Any, | |
| Callable, | |
| ClassVar, | |
| Dict, | |
| Iterable, | |
| Mapping, | |
| MutableMapping, | |
| Optional, | |
| Set, | |
| Union, | |
| ) | |
| log = logging.getLogger(__name__) | |
| __all__ = [ | |
| "AbstractConfig", | |
| "ConfigAlreadyRegisteredError", | |
| "ConfigError", | |
| "ConfigUnknownOptionError", | |
| "ConfigValueParsingError", | |
| "ConfigValueValidationError", | |
| "Option", | |
| "Options", | |
| ] | |
| class ConfigError(Exception): | |
| """Base exception for the config module.""" | |
| class ConfigAlreadyRegisteredError(ConfigError): | |
| """Raised when a module tries to register a configuration option that | |
| already exists. | |
| Should not be raised too much really, only when developing new fontTools | |
| modules. | |
| """ | |
| def __init__(self, name): | |
| super().__init__(f"Config option {name} is already registered.") | |
| class ConfigValueParsingError(ConfigError): | |
| """Raised when a configuration value cannot be parsed.""" | |
| def __init__(self, name, value): | |
| super().__init__( | |
| f"Config option {name}: value cannot be parsed (given {repr(value)})" | |
| ) | |
| class ConfigValueValidationError(ConfigError): | |
| """Raised when a configuration value cannot be validated.""" | |
| def __init__(self, name, value): | |
| super().__init__( | |
| f"Config option {name}: value is invalid (given {repr(value)})" | |
| ) | |
| class ConfigUnknownOptionError(ConfigError): | |
| """Raised when a configuration option is unknown.""" | |
| def __init__(self, option_or_name): | |
| name = ( | |
| f"'{option_or_name.name}' (id={id(option_or_name)})>" | |
| if isinstance(option_or_name, Option) | |
| else f"'{option_or_name}'" | |
| ) | |
| super().__init__(f"Config option {name} is unknown") | |
| # eq=False because Options are unique, not fungible objects | |
| class Option: | |
| name: str | |
| """Unique name identifying the option (e.g. package.module:MY_OPTION).""" | |
| help: str | |
| """Help text for this option.""" | |
| default: Any | |
| """Default value for this option.""" | |
| parse: Callable[[str], Any] | |
| """Turn input (e.g. string) into proper type. Only when reading from file.""" | |
| validate: Optional[Callable[[Any], bool]] = None | |
| """Return true if the given value is an acceptable value.""" | |
| def parse_optional_bool(v: str) -> Optional[bool]: | |
| s = str(v).lower() | |
| if s in {"0", "no", "false"}: | |
| return False | |
| if s in {"1", "yes", "true"}: | |
| return True | |
| if s in {"auto", "none"}: | |
| return None | |
| raise ValueError("invalid optional bool: {v!r}") | |
| def validate_optional_bool(v: Any) -> bool: | |
| return v is None or isinstance(v, bool) | |
| class Options(Mapping): | |
| """Registry of available options for a given config system. | |
| Define new options using the :meth:`register()` method. | |
| Access existing options using the Mapping interface. | |
| """ | |
| __options: Dict[str, Option] | |
| def __init__(self, other: "Options" = None) -> None: | |
| self.__options = {} | |
| if other is not None: | |
| for option in other.values(): | |
| self.register_option(option) | |
| def register( | |
| self, | |
| name: str, | |
| help: str, | |
| default: Any, | |
| parse: Callable[[str], Any], | |
| validate: Optional[Callable[[Any], bool]] = None, | |
| ) -> Option: | |
| """Create and register a new option.""" | |
| return self.register_option(Option(name, help, default, parse, validate)) | |
| def register_option(self, option: Option) -> Option: | |
| """Register a new option.""" | |
| name = option.name | |
| if name in self.__options: | |
| raise ConfigAlreadyRegisteredError(name) | |
| self.__options[name] = option | |
| return option | |
| def is_registered(self, option: Option) -> bool: | |
| """Return True if the same option object is already registered.""" | |
| return self.__options.get(option.name) is option | |
| def __getitem__(self, key: str) -> Option: | |
| return self.__options.__getitem__(key) | |
| def __iter__(self) -> Iterator[str]: | |
| return self.__options.__iter__() | |
| def __len__(self) -> int: | |
| return self.__options.__len__() | |
| def __repr__(self) -> str: | |
| return ( | |
| f"{self.__class__.__name__}({{\n" | |
| + "".join( | |
| f" {k!r}: Option(default={v.default!r}, ...),\n" | |
| for k, v in self.__options.items() | |
| ) | |
| + "})" | |
| ) | |
| _USE_GLOBAL_DEFAULT = object() | |
| class AbstractConfig(MutableMapping): | |
| """ | |
| Create a set of config values, optionally pre-filled with values from | |
| the given dictionary or pre-existing config object. | |
| The class implements the MutableMapping protocol keyed by option name (`str`). | |
| For convenience its methods accept either Option or str as the key parameter. | |
| .. seealso:: :meth:`set()` | |
| This config class is abstract because it needs its ``options`` class | |
| var to be set to an instance of :class:`Options` before it can be | |
| instanciated and used. | |
| .. code:: python | |
| class MyConfig(AbstractConfig): | |
| options = Options() | |
| MyConfig.register_option( "test:option_name", "This is an option", 0, int, lambda v: isinstance(v, int)) | |
| cfg = MyConfig({"test:option_name": 10}) | |
| """ | |
| options: ClassVar[Options] | |
| def register_option( | |
| cls, | |
| name: str, | |
| help: str, | |
| default: Any, | |
| parse: Callable[[str], Any], | |
| validate: Optional[Callable[[Any], bool]] = None, | |
| ) -> Option: | |
| """Register an available option in this config system.""" | |
| return cls.options.register( | |
| name, help=help, default=default, parse=parse, validate=validate | |
| ) | |
| _values: Dict[str, Any] | |
| def __init__( | |
| self, | |
| values: Union[AbstractConfig, Dict[Union[Option, str], Any]] = {}, | |
| parse_values: bool = False, | |
| skip_unknown: bool = False, | |
| ): | |
| self._values = {} | |
| values_dict = values._values if isinstance(values, AbstractConfig) else values | |
| for name, value in values_dict.items(): | |
| self.set(name, value, parse_values, skip_unknown) | |
| def _resolve_option(self, option_or_name: Union[Option, str]) -> Option: | |
| if isinstance(option_or_name, Option): | |
| option = option_or_name | |
| if not self.options.is_registered(option): | |
| raise ConfigUnknownOptionError(option) | |
| return option | |
| elif isinstance(option_or_name, str): | |
| name = option_or_name | |
| try: | |
| return self.options[name] | |
| except KeyError: | |
| raise ConfigUnknownOptionError(name) | |
| else: | |
| raise TypeError( | |
| "expected Option or str, found " | |
| f"{type(option_or_name).__name__}: {option_or_name!r}" | |
| ) | |
| def set( | |
| self, | |
| option_or_name: Union[Option, str], | |
| value: Any, | |
| parse_values: bool = False, | |
| skip_unknown: bool = False, | |
| ): | |
| """Set the value of an option. | |
| Args: | |
| * `option_or_name`: an `Option` object or its name (`str`). | |
| * `value`: the value to be assigned to given option. | |
| * `parse_values`: parse the configuration value from a string into | |
| its proper type, as per its `Option` object. The default | |
| behavior is to raise `ConfigValueValidationError` when the value | |
| is not of the right type. Useful when reading options from a | |
| file type that doesn't support as many types as Python. | |
| * `skip_unknown`: skip unknown configuration options. The default | |
| behaviour is to raise `ConfigUnknownOptionError`. Useful when | |
| reading options from a configuration file that has extra entries | |
| (e.g. for a later version of fontTools) | |
| """ | |
| try: | |
| option = self._resolve_option(option_or_name) | |
| except ConfigUnknownOptionError as e: | |
| if skip_unknown: | |
| log.debug(str(e)) | |
| return | |
| raise | |
| # Can be useful if the values come from a source that doesn't have | |
| # strict typing (.ini file? Terminal input?) | |
| if parse_values: | |
| try: | |
| value = option.parse(value) | |
| except Exception as e: | |
| raise ConfigValueParsingError(option.name, value) from e | |
| if option.validate is not None and not option.validate(value): | |
| raise ConfigValueValidationError(option.name, value) | |
| self._values[option.name] = value | |
| def get( | |
| self, option_or_name: Union[Option, str], default: Any = _USE_GLOBAL_DEFAULT | |
| ) -> Any: | |
| """ | |
| Get the value of an option. The value which is returned is the first | |
| provided among: | |
| 1. a user-provided value in the options's ``self._values`` dict | |
| 2. a caller-provided default value to this method call | |
| 3. the global default for the option provided in ``fontTools.config`` | |
| This is to provide the ability to migrate progressively from config | |
| options passed as arguments to fontTools APIs to config options read | |
| from the current TTFont, e.g. | |
| .. code:: python | |
| def fontToolsAPI(font, some_option): | |
| value = font.cfg.get("someLib.module:SOME_OPTION", some_option) | |
| # use value | |
| That way, the function will work the same for users of the API that | |
| still pass the option to the function call, but will favour the new | |
| config mechanism if the given font specifies a value for that option. | |
| """ | |
| option = self._resolve_option(option_or_name) | |
| if option.name in self._values: | |
| return self._values[option.name] | |
| if default is not _USE_GLOBAL_DEFAULT: | |
| return default | |
| return option.default | |
| def copy(self): | |
| return self.__class__(self._values) | |
| def __getitem__(self, option_or_name: Union[Option, str]) -> Any: | |
| return self.get(option_or_name) | |
| def __setitem__(self, option_or_name: Union[Option, str], value: Any) -> None: | |
| return self.set(option_or_name, value) | |
| def __delitem__(self, option_or_name: Union[Option, str]) -> None: | |
| option = self._resolve_option(option_or_name) | |
| del self._values[option.name] | |
| def __iter__(self) -> Iterable[str]: | |
| return self._values.__iter__() | |
| def __len__(self) -> int: | |
| return len(self._values) | |
| def __repr__(self) -> str: | |
| return f"{self.__class__.__name__}({repr(self._values)})" | |