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