Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/misc/configTools.py: 53%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

134 statements  

1""" 

2Code of the config system; not related to fontTools or fonts in particular. 

3 

4The options that are specific to fontTools are in :mod:`fontTools.config`. 

5 

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. 

9 

10""" 

11 

12from __future__ import annotations 

13 

14import logging 

15from dataclasses import dataclass 

16from typing import ( 

17 Any, 

18 Callable, 

19 ClassVar, 

20 Dict, 

21 Iterable, 

22 Mapping, 

23 MutableMapping, 

24 Optional, 

25 Set, 

26 Union, 

27) 

28 

29 

30log = logging.getLogger(__name__) 

31 

32__all__ = [ 

33 "AbstractConfig", 

34 "ConfigAlreadyRegisteredError", 

35 "ConfigError", 

36 "ConfigUnknownOptionError", 

37 "ConfigValueParsingError", 

38 "ConfigValueValidationError", 

39 "Option", 

40 "Options", 

41] 

42 

43 

44class ConfigError(Exception): 

45 """Base exception for the config module.""" 

46 

47 

48class ConfigAlreadyRegisteredError(ConfigError): 

49 """Raised when a module tries to register a configuration option that 

50 already exists. 

51 

52 Should not be raised too much really, only when developing new fontTools 

53 modules. 

54 """ 

55 

56 def __init__(self, name): 

57 super().__init__(f"Config option {name} is already registered.") 

58 

59 

60class ConfigValueParsingError(ConfigError): 

61 """Raised when a configuration value cannot be parsed.""" 

62 

63 def __init__(self, name, value): 

64 super().__init__( 

65 f"Config option {name}: value cannot be parsed (given {repr(value)})" 

66 ) 

67 

68 

69class ConfigValueValidationError(ConfigError): 

70 """Raised when a configuration value cannot be validated.""" 

71 

72 def __init__(self, name, value): 

73 super().__init__( 

74 f"Config option {name}: value is invalid (given {repr(value)})" 

75 ) 

76 

77 

78class ConfigUnknownOptionError(ConfigError): 

79 """Raised when a configuration option is unknown.""" 

80 

81 def __init__(self, option_or_name): 

82 name = ( 

83 f"'{option_or_name.name}' (id={id(option_or_name)})>" 

84 if isinstance(option_or_name, Option) 

85 else f"'{option_or_name}'" 

86 ) 

87 super().__init__(f"Config option {name} is unknown") 

88 

89 

90# eq=False because Options are unique, not fungible objects 

91@dataclass(frozen=True, eq=False) 

92class Option: 

93 name: str 

94 """Unique name identifying the option (e.g. package.module:MY_OPTION).""" 

95 help: str 

96 """Help text for this option.""" 

97 default: Any 

98 """Default value for this option.""" 

99 parse: Callable[[str], Any] 

100 """Turn input (e.g. string) into proper type. Only when reading from file.""" 

101 validate: Optional[Callable[[Any], bool]] = None 

102 """Return true if the given value is an acceptable value.""" 

103 

104 @staticmethod 

105 def parse_optional_bool(v: str) -> Optional[bool]: 

106 s = str(v).lower() 

107 if s in {"0", "no", "false"}: 

108 return False 

109 if s in {"1", "yes", "true"}: 

110 return True 

111 if s in {"auto", "none"}: 

112 return None 

113 raise ValueError("invalid optional bool: {v!r}") 

114 

115 @staticmethod 

116 def validate_optional_bool(v: Any) -> bool: 

117 return v is None or isinstance(v, bool) 

118 

119 

120class Options(Mapping): 

121 """Registry of available options for a given config system. 

122 

123 Define new options using the :meth:`register()` method. 

124 

125 Access existing options using the Mapping interface. 

126 """ 

127 

128 __options: Dict[str, Option] 

129 

130 def __init__(self, other: "Options" = None) -> None: 

131 self.__options = {} 

132 if other is not None: 

133 for option in other.values(): 

134 self.register_option(option) 

135 

136 def register( 

137 self, 

138 name: str, 

139 help: str, 

140 default: Any, 

141 parse: Callable[[str], Any], 

142 validate: Optional[Callable[[Any], bool]] = None, 

143 ) -> Option: 

144 """Create and register a new option.""" 

145 return self.register_option(Option(name, help, default, parse, validate)) 

146 

147 def register_option(self, option: Option) -> Option: 

148 """Register a new option.""" 

149 name = option.name 

150 if name in self.__options: 

151 raise ConfigAlreadyRegisteredError(name) 

152 self.__options[name] = option 

153 return option 

154 

155 def is_registered(self, option: Option) -> bool: 

156 """Return True if the same option object is already registered.""" 

157 return self.__options.get(option.name) is option 

158 

159 def __getitem__(self, key: str) -> Option: 

160 return self.__options.__getitem__(key) 

161 

162 def __iter__(self) -> Iterator[str]: 

163 return self.__options.__iter__() 

164 

165 def __len__(self) -> int: 

166 return self.__options.__len__() 

167 

168 def __repr__(self) -> str: 

169 return ( 

170 f"{self.__class__.__name__}({{\n" 

171 + "".join( 

172 f" {k!r}: Option(default={v.default!r}, ...),\n" 

173 for k, v in self.__options.items() 

174 ) 

175 + "})" 

176 ) 

177 

178 

179_USE_GLOBAL_DEFAULT = object() 

180 

181 

182class AbstractConfig(MutableMapping): 

183 """ 

184 Create a set of config values, optionally pre-filled with values from 

185 the given dictionary or pre-existing config object. 

186 

187 The class implements the MutableMapping protocol keyed by option name (`str`). 

188 For convenience its methods accept either Option or str as the key parameter. 

189 

190 .. seealso:: :meth:`set()` 

191 

192 This config class is abstract because it needs its ``options`` class 

193 var to be set to an instance of :class:`Options` before it can be 

194 instanciated and used. 

195 

196 .. code:: python 

197 

198 class MyConfig(AbstractConfig): 

199 options = Options() 

200 

201 MyConfig.register_option( "test:option_name", "This is an option", 0, int, lambda v: isinstance(v, int)) 

202 

203 cfg = MyConfig({"test:option_name": 10}) 

204 

205 """ 

206 

207 options: ClassVar[Options] 

208 

209 @classmethod 

210 def register_option( 

211 cls, 

212 name: str, 

213 help: str, 

214 default: Any, 

215 parse: Callable[[str], Any], 

216 validate: Optional[Callable[[Any], bool]] = None, 

217 ) -> Option: 

218 """Register an available option in this config system.""" 

219 return cls.options.register( 

220 name, help=help, default=default, parse=parse, validate=validate 

221 ) 

222 

223 _values: Dict[str, Any] 

224 

225 def __init__( 

226 self, 

227 values: Union[ 

228 AbstractConfig, Dict[Union[Option, str], Any], Mapping[str, Any] 

229 ] = {}, 

230 parse_values: bool = False, 

231 skip_unknown: bool = False, 

232 ): 

233 self._values = {} 

234 values_dict = values._values if isinstance(values, AbstractConfig) else values 

235 for name, value in values_dict.items(): 

236 self.set(name, value, parse_values, skip_unknown) 

237 

238 def _resolve_option(self, option_or_name: Union[Option, str]) -> Option: 

239 if isinstance(option_or_name, Option): 

240 option = option_or_name 

241 if not self.options.is_registered(option): 

242 raise ConfigUnknownOptionError(option) 

243 return option 

244 elif isinstance(option_or_name, str): 

245 name = option_or_name 

246 try: 

247 return self.options[name] 

248 except KeyError: 

249 raise ConfigUnknownOptionError(name) 

250 else: 

251 raise TypeError( 

252 "expected Option or str, found " 

253 f"{type(option_or_name).__name__}: {option_or_name!r}" 

254 ) 

255 

256 def set( 

257 self, 

258 option_or_name: Union[Option, str], 

259 value: Any, 

260 parse_values: bool = False, 

261 skip_unknown: bool = False, 

262 ): 

263 """Set the value of an option. 

264 

265 Args: 

266 * `option_or_name`: an `Option` object or its name (`str`). 

267 * `value`: the value to be assigned to given option. 

268 * `parse_values`: parse the configuration value from a string into 

269 its proper type, as per its `Option` object. The default 

270 behavior is to raise `ConfigValueValidationError` when the value 

271 is not of the right type. Useful when reading options from a 

272 file type that doesn't support as many types as Python. 

273 * `skip_unknown`: skip unknown configuration options. The default 

274 behaviour is to raise `ConfigUnknownOptionError`. Useful when 

275 reading options from a configuration file that has extra entries 

276 (e.g. for a later version of fontTools) 

277 """ 

278 try: 

279 option = self._resolve_option(option_or_name) 

280 except ConfigUnknownOptionError as e: 

281 if skip_unknown: 

282 log.debug(str(e)) 

283 return 

284 raise 

285 

286 # Can be useful if the values come from a source that doesn't have 

287 # strict typing (.ini file? Terminal input?) 

288 if parse_values: 

289 try: 

290 value = option.parse(value) 

291 except Exception as e: 

292 raise ConfigValueParsingError(option.name, value) from e 

293 

294 if option.validate is not None and not option.validate(value): 

295 raise ConfigValueValidationError(option.name, value) 

296 

297 self._values[option.name] = value 

298 

299 def get( 

300 self, option_or_name: Union[Option, str], default: Any = _USE_GLOBAL_DEFAULT 

301 ) -> Any: 

302 """ 

303 Get the value of an option. The value which is returned is the first 

304 provided among: 

305 

306 1. a user-provided value in the options's ``self._values`` dict 

307 2. a caller-provided default value to this method call 

308 3. the global default for the option provided in ``fontTools.config`` 

309 

310 This is to provide the ability to migrate progressively from config 

311 options passed as arguments to fontTools APIs to config options read 

312 from the current TTFont, e.g. 

313 

314 .. code:: python 

315 

316 def fontToolsAPI(font, some_option): 

317 value = font.cfg.get("someLib.module:SOME_OPTION", some_option) 

318 # use value 

319 

320 That way, the function will work the same for users of the API that 

321 still pass the option to the function call, but will favour the new 

322 config mechanism if the given font specifies a value for that option. 

323 """ 

324 option = self._resolve_option(option_or_name) 

325 if option.name in self._values: 

326 return self._values[option.name] 

327 if default is not _USE_GLOBAL_DEFAULT: 

328 return default 

329 return option.default 

330 

331 def copy(self): 

332 return self.__class__(self._values) 

333 

334 def __getitem__(self, option_or_name: Union[Option, str]) -> Any: 

335 return self.get(option_or_name) 

336 

337 def __setitem__(self, option_or_name: Union[Option, str], value: Any) -> None: 

338 return self.set(option_or_name, value) 

339 

340 def __delitem__(self, option_or_name: Union[Option, str]) -> None: 

341 option = self._resolve_option(option_or_name) 

342 del self._values[option.name] 

343 

344 def __iter__(self) -> Iterable[str]: 

345 return self._values.__iter__() 

346 

347 def __len__(self) -> int: 

348 return len(self._values) 

349 

350 def __repr__(self) -> str: 

351 return f"{self.__class__.__name__}({repr(self._values)})"