xref: /aosp_15_r20/external/fonttools/Lib/fontTools/misc/configTools.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""
2*e1fe3e4aSElliott HughesCode of the config system; not related to fontTools or fonts in particular.
3*e1fe3e4aSElliott Hughes
4*e1fe3e4aSElliott HughesThe options that are specific to fontTools are in :mod:`fontTools.config`.
5*e1fe3e4aSElliott Hughes
6*e1fe3e4aSElliott HughesTo create your own config system, you need to create an instance of
7*e1fe3e4aSElliott Hughes:class:`Options`, and a subclass of :class:`AbstractConfig` with its
8*e1fe3e4aSElliott Hughes``options`` class variable set to your instance of Options.
9*e1fe3e4aSElliott Hughes
10*e1fe3e4aSElliott Hughes"""
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott Hughesfrom __future__ import annotations
13*e1fe3e4aSElliott Hughes
14*e1fe3e4aSElliott Hughesimport logging
15*e1fe3e4aSElliott Hughesfrom dataclasses import dataclass
16*e1fe3e4aSElliott Hughesfrom typing import (
17*e1fe3e4aSElliott Hughes    Any,
18*e1fe3e4aSElliott Hughes    Callable,
19*e1fe3e4aSElliott Hughes    ClassVar,
20*e1fe3e4aSElliott Hughes    Dict,
21*e1fe3e4aSElliott Hughes    Iterable,
22*e1fe3e4aSElliott Hughes    Mapping,
23*e1fe3e4aSElliott Hughes    MutableMapping,
24*e1fe3e4aSElliott Hughes    Optional,
25*e1fe3e4aSElliott Hughes    Set,
26*e1fe3e4aSElliott Hughes    Union,
27*e1fe3e4aSElliott Hughes)
28*e1fe3e4aSElliott Hughes
29*e1fe3e4aSElliott Hughes
30*e1fe3e4aSElliott Hugheslog = logging.getLogger(__name__)
31*e1fe3e4aSElliott Hughes
32*e1fe3e4aSElliott Hughes__all__ = [
33*e1fe3e4aSElliott Hughes    "AbstractConfig",
34*e1fe3e4aSElliott Hughes    "ConfigAlreadyRegisteredError",
35*e1fe3e4aSElliott Hughes    "ConfigError",
36*e1fe3e4aSElliott Hughes    "ConfigUnknownOptionError",
37*e1fe3e4aSElliott Hughes    "ConfigValueParsingError",
38*e1fe3e4aSElliott Hughes    "ConfigValueValidationError",
39*e1fe3e4aSElliott Hughes    "Option",
40*e1fe3e4aSElliott Hughes    "Options",
41*e1fe3e4aSElliott Hughes]
42*e1fe3e4aSElliott Hughes
43*e1fe3e4aSElliott Hughes
44*e1fe3e4aSElliott Hughesclass ConfigError(Exception):
45*e1fe3e4aSElliott Hughes    """Base exception for the config module."""
46*e1fe3e4aSElliott Hughes
47*e1fe3e4aSElliott Hughes
48*e1fe3e4aSElliott Hughesclass ConfigAlreadyRegisteredError(ConfigError):
49*e1fe3e4aSElliott Hughes    """Raised when a module tries to register a configuration option that
50*e1fe3e4aSElliott Hughes    already exists.
51*e1fe3e4aSElliott Hughes
52*e1fe3e4aSElliott Hughes    Should not be raised too much really, only when developing new fontTools
53*e1fe3e4aSElliott Hughes    modules.
54*e1fe3e4aSElliott Hughes    """
55*e1fe3e4aSElliott Hughes
56*e1fe3e4aSElliott Hughes    def __init__(self, name):
57*e1fe3e4aSElliott Hughes        super().__init__(f"Config option {name} is already registered.")
58*e1fe3e4aSElliott Hughes
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughesclass ConfigValueParsingError(ConfigError):
61*e1fe3e4aSElliott Hughes    """Raised when a configuration value cannot be parsed."""
62*e1fe3e4aSElliott Hughes
63*e1fe3e4aSElliott Hughes    def __init__(self, name, value):
64*e1fe3e4aSElliott Hughes        super().__init__(
65*e1fe3e4aSElliott Hughes            f"Config option {name}: value cannot be parsed (given {repr(value)})"
66*e1fe3e4aSElliott Hughes        )
67*e1fe3e4aSElliott Hughes
68*e1fe3e4aSElliott Hughes
69*e1fe3e4aSElliott Hughesclass ConfigValueValidationError(ConfigError):
70*e1fe3e4aSElliott Hughes    """Raised when a configuration value cannot be validated."""
71*e1fe3e4aSElliott Hughes
72*e1fe3e4aSElliott Hughes    def __init__(self, name, value):
73*e1fe3e4aSElliott Hughes        super().__init__(
74*e1fe3e4aSElliott Hughes            f"Config option {name}: value is invalid (given {repr(value)})"
75*e1fe3e4aSElliott Hughes        )
76*e1fe3e4aSElliott Hughes
77*e1fe3e4aSElliott Hughes
78*e1fe3e4aSElliott Hughesclass ConfigUnknownOptionError(ConfigError):
79*e1fe3e4aSElliott Hughes    """Raised when a configuration option is unknown."""
80*e1fe3e4aSElliott Hughes
81*e1fe3e4aSElliott Hughes    def __init__(self, option_or_name):
82*e1fe3e4aSElliott Hughes        name = (
83*e1fe3e4aSElliott Hughes            f"'{option_or_name.name}' (id={id(option_or_name)})>"
84*e1fe3e4aSElliott Hughes            if isinstance(option_or_name, Option)
85*e1fe3e4aSElliott Hughes            else f"'{option_or_name}'"
86*e1fe3e4aSElliott Hughes        )
87*e1fe3e4aSElliott Hughes        super().__init__(f"Config option {name} is unknown")
88*e1fe3e4aSElliott Hughes
89*e1fe3e4aSElliott Hughes
90*e1fe3e4aSElliott Hughes# eq=False because Options are unique, not fungible objects
91*e1fe3e4aSElliott Hughes@dataclass(frozen=True, eq=False)
92*e1fe3e4aSElliott Hughesclass Option:
93*e1fe3e4aSElliott Hughes    name: str
94*e1fe3e4aSElliott Hughes    """Unique name identifying the option (e.g. package.module:MY_OPTION)."""
95*e1fe3e4aSElliott Hughes    help: str
96*e1fe3e4aSElliott Hughes    """Help text for this option."""
97*e1fe3e4aSElliott Hughes    default: Any
98*e1fe3e4aSElliott Hughes    """Default value for this option."""
99*e1fe3e4aSElliott Hughes    parse: Callable[[str], Any]
100*e1fe3e4aSElliott Hughes    """Turn input (e.g. string) into proper type. Only when reading from file."""
101*e1fe3e4aSElliott Hughes    validate: Optional[Callable[[Any], bool]] = None
102*e1fe3e4aSElliott Hughes    """Return true if the given value is an acceptable value."""
103*e1fe3e4aSElliott Hughes
104*e1fe3e4aSElliott Hughes    @staticmethod
105*e1fe3e4aSElliott Hughes    def parse_optional_bool(v: str) -> Optional[bool]:
106*e1fe3e4aSElliott Hughes        s = str(v).lower()
107*e1fe3e4aSElliott Hughes        if s in {"0", "no", "false"}:
108*e1fe3e4aSElliott Hughes            return False
109*e1fe3e4aSElliott Hughes        if s in {"1", "yes", "true"}:
110*e1fe3e4aSElliott Hughes            return True
111*e1fe3e4aSElliott Hughes        if s in {"auto", "none"}:
112*e1fe3e4aSElliott Hughes            return None
113*e1fe3e4aSElliott Hughes        raise ValueError("invalid optional bool: {v!r}")
114*e1fe3e4aSElliott Hughes
115*e1fe3e4aSElliott Hughes    @staticmethod
116*e1fe3e4aSElliott Hughes    def validate_optional_bool(v: Any) -> bool:
117*e1fe3e4aSElliott Hughes        return v is None or isinstance(v, bool)
118*e1fe3e4aSElliott Hughes
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughesclass Options(Mapping):
121*e1fe3e4aSElliott Hughes    """Registry of available options for a given config system.
122*e1fe3e4aSElliott Hughes
123*e1fe3e4aSElliott Hughes    Define new options using the :meth:`register()` method.
124*e1fe3e4aSElliott Hughes
125*e1fe3e4aSElliott Hughes    Access existing options using the Mapping interface.
126*e1fe3e4aSElliott Hughes    """
127*e1fe3e4aSElliott Hughes
128*e1fe3e4aSElliott Hughes    __options: Dict[str, Option]
129*e1fe3e4aSElliott Hughes
130*e1fe3e4aSElliott Hughes    def __init__(self, other: "Options" = None) -> None:
131*e1fe3e4aSElliott Hughes        self.__options = {}
132*e1fe3e4aSElliott Hughes        if other is not None:
133*e1fe3e4aSElliott Hughes            for option in other.values():
134*e1fe3e4aSElliott Hughes                self.register_option(option)
135*e1fe3e4aSElliott Hughes
136*e1fe3e4aSElliott Hughes    def register(
137*e1fe3e4aSElliott Hughes        self,
138*e1fe3e4aSElliott Hughes        name: str,
139*e1fe3e4aSElliott Hughes        help: str,
140*e1fe3e4aSElliott Hughes        default: Any,
141*e1fe3e4aSElliott Hughes        parse: Callable[[str], Any],
142*e1fe3e4aSElliott Hughes        validate: Optional[Callable[[Any], bool]] = None,
143*e1fe3e4aSElliott Hughes    ) -> Option:
144*e1fe3e4aSElliott Hughes        """Create and register a new option."""
145*e1fe3e4aSElliott Hughes        return self.register_option(Option(name, help, default, parse, validate))
146*e1fe3e4aSElliott Hughes
147*e1fe3e4aSElliott Hughes    def register_option(self, option: Option) -> Option:
148*e1fe3e4aSElliott Hughes        """Register a new option."""
149*e1fe3e4aSElliott Hughes        name = option.name
150*e1fe3e4aSElliott Hughes        if name in self.__options:
151*e1fe3e4aSElliott Hughes            raise ConfigAlreadyRegisteredError(name)
152*e1fe3e4aSElliott Hughes        self.__options[name] = option
153*e1fe3e4aSElliott Hughes        return option
154*e1fe3e4aSElliott Hughes
155*e1fe3e4aSElliott Hughes    def is_registered(self, option: Option) -> bool:
156*e1fe3e4aSElliott Hughes        """Return True if the same option object is already registered."""
157*e1fe3e4aSElliott Hughes        return self.__options.get(option.name) is option
158*e1fe3e4aSElliott Hughes
159*e1fe3e4aSElliott Hughes    def __getitem__(self, key: str) -> Option:
160*e1fe3e4aSElliott Hughes        return self.__options.__getitem__(key)
161*e1fe3e4aSElliott Hughes
162*e1fe3e4aSElliott Hughes    def __iter__(self) -> Iterator[str]:
163*e1fe3e4aSElliott Hughes        return self.__options.__iter__()
164*e1fe3e4aSElliott Hughes
165*e1fe3e4aSElliott Hughes    def __len__(self) -> int:
166*e1fe3e4aSElliott Hughes        return self.__options.__len__()
167*e1fe3e4aSElliott Hughes
168*e1fe3e4aSElliott Hughes    def __repr__(self) -> str:
169*e1fe3e4aSElliott Hughes        return (
170*e1fe3e4aSElliott Hughes            f"{self.__class__.__name__}({{\n"
171*e1fe3e4aSElliott Hughes            + "".join(
172*e1fe3e4aSElliott Hughes                f"    {k!r}: Option(default={v.default!r}, ...),\n"
173*e1fe3e4aSElliott Hughes                for k, v in self.__options.items()
174*e1fe3e4aSElliott Hughes            )
175*e1fe3e4aSElliott Hughes            + "})"
176*e1fe3e4aSElliott Hughes        )
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughes
179*e1fe3e4aSElliott Hughes_USE_GLOBAL_DEFAULT = object()
180*e1fe3e4aSElliott Hughes
181*e1fe3e4aSElliott Hughes
182*e1fe3e4aSElliott Hughesclass AbstractConfig(MutableMapping):
183*e1fe3e4aSElliott Hughes    """
184*e1fe3e4aSElliott Hughes    Create a set of config values, optionally pre-filled with values from
185*e1fe3e4aSElliott Hughes    the given dictionary or pre-existing config object.
186*e1fe3e4aSElliott Hughes
187*e1fe3e4aSElliott Hughes    The class implements the MutableMapping protocol keyed by option name (`str`).
188*e1fe3e4aSElliott Hughes    For convenience its methods accept either Option or str as the key parameter.
189*e1fe3e4aSElliott Hughes
190*e1fe3e4aSElliott Hughes    .. seealso:: :meth:`set()`
191*e1fe3e4aSElliott Hughes
192*e1fe3e4aSElliott Hughes    This config class is abstract because it needs its ``options`` class
193*e1fe3e4aSElliott Hughes    var to be set to an instance of :class:`Options` before it can be
194*e1fe3e4aSElliott Hughes    instanciated and used.
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes    .. code:: python
197*e1fe3e4aSElliott Hughes
198*e1fe3e4aSElliott Hughes        class MyConfig(AbstractConfig):
199*e1fe3e4aSElliott Hughes            options = Options()
200*e1fe3e4aSElliott Hughes
201*e1fe3e4aSElliott Hughes        MyConfig.register_option( "test:option_name", "This is an option", 0, int, lambda v: isinstance(v, int))
202*e1fe3e4aSElliott Hughes
203*e1fe3e4aSElliott Hughes        cfg = MyConfig({"test:option_name": 10})
204*e1fe3e4aSElliott Hughes
205*e1fe3e4aSElliott Hughes    """
206*e1fe3e4aSElliott Hughes
207*e1fe3e4aSElliott Hughes    options: ClassVar[Options]
208*e1fe3e4aSElliott Hughes
209*e1fe3e4aSElliott Hughes    @classmethod
210*e1fe3e4aSElliott Hughes    def register_option(
211*e1fe3e4aSElliott Hughes        cls,
212*e1fe3e4aSElliott Hughes        name: str,
213*e1fe3e4aSElliott Hughes        help: str,
214*e1fe3e4aSElliott Hughes        default: Any,
215*e1fe3e4aSElliott Hughes        parse: Callable[[str], Any],
216*e1fe3e4aSElliott Hughes        validate: Optional[Callable[[Any], bool]] = None,
217*e1fe3e4aSElliott Hughes    ) -> Option:
218*e1fe3e4aSElliott Hughes        """Register an available option in this config system."""
219*e1fe3e4aSElliott Hughes        return cls.options.register(
220*e1fe3e4aSElliott Hughes            name, help=help, default=default, parse=parse, validate=validate
221*e1fe3e4aSElliott Hughes        )
222*e1fe3e4aSElliott Hughes
223*e1fe3e4aSElliott Hughes    _values: Dict[str, Any]
224*e1fe3e4aSElliott Hughes
225*e1fe3e4aSElliott Hughes    def __init__(
226*e1fe3e4aSElliott Hughes        self,
227*e1fe3e4aSElliott Hughes        values: Union[AbstractConfig, Dict[Union[Option, str], Any]] = {},
228*e1fe3e4aSElliott Hughes        parse_values: bool = False,
229*e1fe3e4aSElliott Hughes        skip_unknown: bool = False,
230*e1fe3e4aSElliott Hughes    ):
231*e1fe3e4aSElliott Hughes        self._values = {}
232*e1fe3e4aSElliott Hughes        values_dict = values._values if isinstance(values, AbstractConfig) else values
233*e1fe3e4aSElliott Hughes        for name, value in values_dict.items():
234*e1fe3e4aSElliott Hughes            self.set(name, value, parse_values, skip_unknown)
235*e1fe3e4aSElliott Hughes
236*e1fe3e4aSElliott Hughes    def _resolve_option(self, option_or_name: Union[Option, str]) -> Option:
237*e1fe3e4aSElliott Hughes        if isinstance(option_or_name, Option):
238*e1fe3e4aSElliott Hughes            option = option_or_name
239*e1fe3e4aSElliott Hughes            if not self.options.is_registered(option):
240*e1fe3e4aSElliott Hughes                raise ConfigUnknownOptionError(option)
241*e1fe3e4aSElliott Hughes            return option
242*e1fe3e4aSElliott Hughes        elif isinstance(option_or_name, str):
243*e1fe3e4aSElliott Hughes            name = option_or_name
244*e1fe3e4aSElliott Hughes            try:
245*e1fe3e4aSElliott Hughes                return self.options[name]
246*e1fe3e4aSElliott Hughes            except KeyError:
247*e1fe3e4aSElliott Hughes                raise ConfigUnknownOptionError(name)
248*e1fe3e4aSElliott Hughes        else:
249*e1fe3e4aSElliott Hughes            raise TypeError(
250*e1fe3e4aSElliott Hughes                "expected Option or str, found "
251*e1fe3e4aSElliott Hughes                f"{type(option_or_name).__name__}: {option_or_name!r}"
252*e1fe3e4aSElliott Hughes            )
253*e1fe3e4aSElliott Hughes
254*e1fe3e4aSElliott Hughes    def set(
255*e1fe3e4aSElliott Hughes        self,
256*e1fe3e4aSElliott Hughes        option_or_name: Union[Option, str],
257*e1fe3e4aSElliott Hughes        value: Any,
258*e1fe3e4aSElliott Hughes        parse_values: bool = False,
259*e1fe3e4aSElliott Hughes        skip_unknown: bool = False,
260*e1fe3e4aSElliott Hughes    ):
261*e1fe3e4aSElliott Hughes        """Set the value of an option.
262*e1fe3e4aSElliott Hughes
263*e1fe3e4aSElliott Hughes        Args:
264*e1fe3e4aSElliott Hughes            * `option_or_name`: an `Option` object or its name (`str`).
265*e1fe3e4aSElliott Hughes            * `value`: the value to be assigned to given option.
266*e1fe3e4aSElliott Hughes            * `parse_values`: parse the configuration value from a string into
267*e1fe3e4aSElliott Hughes                its proper type, as per its `Option` object. The default
268*e1fe3e4aSElliott Hughes                behavior is to raise `ConfigValueValidationError` when the value
269*e1fe3e4aSElliott Hughes                is not of the right type. Useful when reading options from a
270*e1fe3e4aSElliott Hughes                file type that doesn't support as many types as Python.
271*e1fe3e4aSElliott Hughes            * `skip_unknown`: skip unknown configuration options. The default
272*e1fe3e4aSElliott Hughes                behaviour is to raise `ConfigUnknownOptionError`. Useful when
273*e1fe3e4aSElliott Hughes                reading options from a configuration file that has extra entries
274*e1fe3e4aSElliott Hughes                (e.g. for a later version of fontTools)
275*e1fe3e4aSElliott Hughes        """
276*e1fe3e4aSElliott Hughes        try:
277*e1fe3e4aSElliott Hughes            option = self._resolve_option(option_or_name)
278*e1fe3e4aSElliott Hughes        except ConfigUnknownOptionError as e:
279*e1fe3e4aSElliott Hughes            if skip_unknown:
280*e1fe3e4aSElliott Hughes                log.debug(str(e))
281*e1fe3e4aSElliott Hughes                return
282*e1fe3e4aSElliott Hughes            raise
283*e1fe3e4aSElliott Hughes
284*e1fe3e4aSElliott Hughes        # Can be useful if the values come from a source that doesn't have
285*e1fe3e4aSElliott Hughes        # strict typing (.ini file? Terminal input?)
286*e1fe3e4aSElliott Hughes        if parse_values:
287*e1fe3e4aSElliott Hughes            try:
288*e1fe3e4aSElliott Hughes                value = option.parse(value)
289*e1fe3e4aSElliott Hughes            except Exception as e:
290*e1fe3e4aSElliott Hughes                raise ConfigValueParsingError(option.name, value) from e
291*e1fe3e4aSElliott Hughes
292*e1fe3e4aSElliott Hughes        if option.validate is not None and not option.validate(value):
293*e1fe3e4aSElliott Hughes            raise ConfigValueValidationError(option.name, value)
294*e1fe3e4aSElliott Hughes
295*e1fe3e4aSElliott Hughes        self._values[option.name] = value
296*e1fe3e4aSElliott Hughes
297*e1fe3e4aSElliott Hughes    def get(
298*e1fe3e4aSElliott Hughes        self, option_or_name: Union[Option, str], default: Any = _USE_GLOBAL_DEFAULT
299*e1fe3e4aSElliott Hughes    ) -> Any:
300*e1fe3e4aSElliott Hughes        """
301*e1fe3e4aSElliott Hughes        Get the value of an option. The value which is returned is the first
302*e1fe3e4aSElliott Hughes        provided among:
303*e1fe3e4aSElliott Hughes
304*e1fe3e4aSElliott Hughes        1. a user-provided value in the options's ``self._values`` dict
305*e1fe3e4aSElliott Hughes        2. a caller-provided default value to this method call
306*e1fe3e4aSElliott Hughes        3. the global default for the option provided in ``fontTools.config``
307*e1fe3e4aSElliott Hughes
308*e1fe3e4aSElliott Hughes        This is to provide the ability to migrate progressively from config
309*e1fe3e4aSElliott Hughes        options passed as arguments to fontTools APIs to config options read
310*e1fe3e4aSElliott Hughes        from the current TTFont, e.g.
311*e1fe3e4aSElliott Hughes
312*e1fe3e4aSElliott Hughes        .. code:: python
313*e1fe3e4aSElliott Hughes
314*e1fe3e4aSElliott Hughes            def fontToolsAPI(font, some_option):
315*e1fe3e4aSElliott Hughes                value = font.cfg.get("someLib.module:SOME_OPTION", some_option)
316*e1fe3e4aSElliott Hughes                # use value
317*e1fe3e4aSElliott Hughes
318*e1fe3e4aSElliott Hughes        That way, the function will work the same for users of the API that
319*e1fe3e4aSElliott Hughes        still pass the option to the function call, but will favour the new
320*e1fe3e4aSElliott Hughes        config mechanism if the given font specifies a value for that option.
321*e1fe3e4aSElliott Hughes        """
322*e1fe3e4aSElliott Hughes        option = self._resolve_option(option_or_name)
323*e1fe3e4aSElliott Hughes        if option.name in self._values:
324*e1fe3e4aSElliott Hughes            return self._values[option.name]
325*e1fe3e4aSElliott Hughes        if default is not _USE_GLOBAL_DEFAULT:
326*e1fe3e4aSElliott Hughes            return default
327*e1fe3e4aSElliott Hughes        return option.default
328*e1fe3e4aSElliott Hughes
329*e1fe3e4aSElliott Hughes    def copy(self):
330*e1fe3e4aSElliott Hughes        return self.__class__(self._values)
331*e1fe3e4aSElliott Hughes
332*e1fe3e4aSElliott Hughes    def __getitem__(self, option_or_name: Union[Option, str]) -> Any:
333*e1fe3e4aSElliott Hughes        return self.get(option_or_name)
334*e1fe3e4aSElliott Hughes
335*e1fe3e4aSElliott Hughes    def __setitem__(self, option_or_name: Union[Option, str], value: Any) -> None:
336*e1fe3e4aSElliott Hughes        return self.set(option_or_name, value)
337*e1fe3e4aSElliott Hughes
338*e1fe3e4aSElliott Hughes    def __delitem__(self, option_or_name: Union[Option, str]) -> None:
339*e1fe3e4aSElliott Hughes        option = self._resolve_option(option_or_name)
340*e1fe3e4aSElliott Hughes        del self._values[option.name]
341*e1fe3e4aSElliott Hughes
342*e1fe3e4aSElliott Hughes    def __iter__(self) -> Iterable[str]:
343*e1fe3e4aSElliott Hughes        return self._values.__iter__()
344*e1fe3e4aSElliott Hughes
345*e1fe3e4aSElliott Hughes    def __len__(self) -> int:
346*e1fe3e4aSElliott Hughes        return len(self._values)
347*e1fe3e4aSElliott Hughes
348*e1fe3e4aSElliott Hughes    def __repr__(self) -> str:
349*e1fe3e4aSElliott Hughes        return f"{self.__class__.__name__}({repr(self._values)})"
350