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