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

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""" 

11from __future__ import annotations 

12 

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) 

27 

28 

29log = logging.getLogger(__name__) 

30 

31__all__ = [ 

32 "AbstractConfig", 

33 "ConfigAlreadyRegisteredError", 

34 "ConfigError", 

35 "ConfigUnknownOptionError", 

36 "ConfigValueParsingError", 

37 "ConfigValueValidationError", 

38 "Option", 

39 "Options", 

40] 

41 

42 

43class ConfigError(Exception): 

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

45 

46 

47class ConfigAlreadyRegisteredError(ConfigError): 

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

49 already exists. 

50 

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

52 modules. 

53 """ 

54 

55 def __init__(self, name): 

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

57 

58 

59class ConfigValueParsingError(ConfigError): 

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

61 

62 def __init__(self, name, value): 

63 super().__init__( 

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

65 ) 

66 

67 

68class ConfigValueValidationError(ConfigError): 

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

70 

71 def __init__(self, name, value): 

72 super().__init__( 

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

74 ) 

75 

76 

77class ConfigUnknownOptionError(ConfigError): 

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

79 

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") 

87 

88 

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.""" 

102 

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}") 

113 

114 @staticmethod 

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

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

117 

118 

119class Options(Mapping): 

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

121 

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

123 

124 Access existing options using the Mapping interface. 

125 """ 

126 

127 __options: Dict[str, Option] 

128 

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) 

134 

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)) 

145 

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 

153 

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 

157 

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

159 return self.__options.__getitem__(key) 

160 

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

162 return self.__options.__iter__() 

163 

164 def __len__(self) -> int: 

165 return self.__options.__len__() 

166 

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 ) 

176 

177 

178_USE_GLOBAL_DEFAULT = object() 

179 

180 

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. 

185 

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. 

188 

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

190 

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. 

194 

195 .. code:: python 

196 

197 class MyConfig(AbstractConfig): 

198 options = Options() 

199 

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

201 

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

203 

204 """ 

205 

206 options: ClassVar[Options] 

207 

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 ) 

221 

222 _values: Dict[str, Any] 

223 

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) 

234 

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 ) 

252 

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. 

261 

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 

282 

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 

290 

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

292 raise ConfigValueValidationError(option.name, value) 

293 

294 self._values[option.name] = value 

295 

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: 

302 

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`` 

306 

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. 

310 

311 .. code:: python 

312 

313 def fontToolsAPI(font, some_option): 

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

315 # use value 

316 

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 

327 

328 def copy(self): 

329 return self.__class__(self._values) 

330 

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

332 return self.get(option_or_name) 

333 

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

335 return self.set(option_or_name, value) 

336 

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] 

340 

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

342 return self._values.__iter__() 

343 

344 def __len__(self) -> int: 

345 return len(self._values) 

346 

347 def __repr__(self) -> str: 

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