aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/misc/configTools.py
diff options
context:
space:
mode:
authorshumkovnd <shumkovnd@yandex-team.com>2023-11-10 14:39:34 +0300
committershumkovnd <shumkovnd@yandex-team.com>2023-11-10 16:42:24 +0300
commit77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch)
treec51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/fonttools/fontTools/misc/configTools.py
parentdd6d20cadb65582270ac23f4b3b14ae189704b9d (diff)
downloadydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/fonttools/fontTools/misc/configTools.py')
-rw-r--r--contrib/python/fonttools/fontTools/misc/configTools.py348
1 files changed, 348 insertions, 0 deletions
diff --git a/contrib/python/fonttools/fontTools/misc/configTools.py b/contrib/python/fonttools/fontTools/misc/configTools.py
new file mode 100644
index 00000000000..38bbada24a1
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/misc/configTools.py
@@ -0,0 +1,348 @@
+"""
+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
+@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
+
+ # 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)})"