up follow livre
This commit is contained in:
parent
70a5c3465c
commit
cffb31c1ef
12198 changed files with 2562132 additions and 35 deletions
349
venv/lib/python3.13/site-packages/fontTools/misc/configTools.py
Normal file
349
venv/lib/python3.13/site-packages/fontTools/misc/configTools.py
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
"""
|
||||
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)})"
|
||||
Loading…
Add table
Add a link
Reference in a new issue