Spaces:
Running
Running
""" | |
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)})" | |