|
""" |
|
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") |
|
|
|
|
|
|
|
@dataclass(frozen=True, eq=False) |
|
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.""" |
|
|
|
@staticmethod |
|
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}") |
|
|
|
@staticmethod |
|
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] |
|
|
|
@classmethod |
|
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 |
|
|
|
|
|
|
|
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)})" |
|
|