Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/conf.py: 100%

226 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-08-27 21:08 -0500

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2024 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24WuttJamaican - app configuration 

25""" 

26 

27import configparser 

28import importlib 

29import logging 

30import logging.config 

31import os 

32import sys 

33import tempfile 

34 

35import config as configuration 

36 

37from wuttjamaican.util import (load_entry_points, load_object, 

38 parse_bool, parse_list, 

39 UNSPECIFIED) 

40from wuttjamaican.exc import ConfigurationError 

41 

42 

43log = logging.getLogger(__name__) 

44 

45 

46class WuttaConfig: 

47 """ 

48 Configuration class for Wutta Framework 

49 

50 A single instance of this class is typically created on app 

51 startup, by calling :func:`make_config()`. 

52 

53 The global config object is mainly responsible for providing 

54 config values to the app, via :meth:`get()` and similar methods. 

55 

56 The config object may have more than one place to look when 

57 finding values. This can vary somewhat but often the priority for 

58 lookup is like: 

59 

60 * settings table in the DB 

61 * one or more INI files 

62 * "defaults" provided by app logic 

63 

64 :param files: List of file paths from which to read config values. 

65 

66 :param defaults: Initial values to use as defaults. This gets 

67 converted to :attr:`defaults` during construction. 

68 

69 :param appname: Value to assign for :attr:`appname`. 

70 

71 :param usedb: Flag indicating whether config values should ever be 

72 looked up from the DB. Note that you can override this when 

73 calling :meth:`get()`. 

74 

75 :param preferdb: Flag indicating whether values from DB should be 

76 preferred over the values from INI files or app defaults. Note 

77 that you can override this when calling :meth:`get()`. 

78 

79 :param configure_logging: Flag indicating whether logging should 

80 be configured during object construction. If not specified, 

81 the config values will determine behavior. 

82 

83 Attributes available on the config instance: 

84 

85 .. attribute:: appname 

86 

87 Code-friendly name ("key") for the app. This is used as the 

88 basis for various config settings and will therefore determine 

89 what is returned from :meth:`get_app()` etc. 

90 

91 For instance the default ``appname`` value is ``'wutta'`` which 

92 means a sample config file might look like: 

93 

94 .. code-block:: ini 

95 

96 [wutta] 

97 app.handler = wuttjamaican.app:AppHandler 

98 

99 [wutta.db] 

100 default.url = sqlite:// 

101 

102 But if the ``appname`` value is e.g. ``'rattail'`` then the 

103 sample config should instead look like: 

104 

105 .. code-block:: ini 

106 

107 [rattail] 

108 app.handler = wuttjamaican.app:AppHandler 

109 

110 [rattail.db] 

111 default.url = sqlite:// 

112 

113 .. attribute:: configuration 

114 

115 Reference to the 

116 :class:`python-configuration:config.ConfigurationSet` instance 

117 which houses the full set of config values which are kept in 

118 memory. This does *not* contain settings from DB, but *does* 

119 contain :attr:`defaults` as well as values read from INI files. 

120 

121 .. attribute:: defaults 

122 

123 Reference to the 

124 :class:`python-configuration:config.Configuration` instance 

125 containing config *default* values. This is exposed in case 

126 it's useful, but in practice you should not update it directly; 

127 instead use :meth:`setdefault()`. 

128 

129 .. attribute:: default_app_handler_spec 

130 

131 Spec string for the default app handler, if config does not 

132 specify to use another. 

133 

134 The true default for this is ``'wuttjamaican.app:AppHandler'`` 

135 (aka. :class:`~wuttjamaican.app.AppHandler`). 

136 

137 .. attribute:: default_engine_maker_spec 

138 

139 Spec string for the default engine maker function, if config 

140 does not specify to use another. 

141 

142 The true default for this is 

143 ``'wuttjamaican.db.conf:make_engine_from_config'`` (aka. 

144 :func:`~wuttjamaican.db.conf.make_engine_from_config()`). 

145 

146 .. attribute:: files_read 

147 

148 List of all INI config files which were read on app startup. 

149 These are listed in the same order as they were read. This 

150 sequence also reflects priority for value lookups, i.e. the 

151 first file with the value wins. 

152 

153 .. attribute:: usedb 

154 

155 Whether the :term:`settings table` should be searched for 

156 config settings. This is ``False`` by default but may be 

157 enabled via config file: 

158 

159 .. code-block:: ini 

160 

161 [wutta.config] 

162 usedb = true 

163 

164 See also :ref:`where-config-settings-come-from`. 

165 

166 .. attribute:: preferdb 

167 

168 Whether the :term:`settings table` should be preferred over 

169 :term:`config files<config file>` when looking for config 

170 settings. This is ``False`` by default, and in any case is 

171 ignored unless :attr:`usedb` is ``True``. 

172 

173 Most apps will want to enable this flag so that when the 

174 settings table is updated, it will immediately affect app 

175 behavior regardless of what values are in the config files. 

176 

177 .. code-block:: ini 

178 

179 [wutta.config] 

180 usedb = true 

181 preferdb = true 

182 

183 See also :ref:`where-config-settings-come-from`. 

184 """ 

185 default_app_handler_spec = 'wuttjamaican.app:AppHandler' 

186 default_engine_maker_spec = 'wuttjamaican.db.conf:make_engine_from_config' 

187 

188 def __init__( 

189 self, 

190 files=[], 

191 defaults={}, 

192 appname='wutta', 

193 usedb=None, 

194 preferdb=None, 

195 configure_logging=None, 

196 ): 

197 self.appname = appname 

198 configs = [] 

199 

200 # read all files requested 

201 self.files_read = [] 

202 for path in files: 

203 self._load_ini_configs(path, configs, require=True) 

204 log.debug("config files were: %s", self.files_read) 

205 

206 # add config for use w/ setdefault() 

207 self.defaults = configuration.Configuration(defaults) 

208 configs.append(self.defaults) 

209 

210 # master config set 

211 self.configuration = configuration.ConfigurationSet(*configs) 

212 

213 # establish logging 

214 if configure_logging is None: 

215 configure_logging = self.get_bool(f'{self.appname}.config.configure_logging', 

216 default=False, usedb=False) 

217 if configure_logging: 

218 self._configure_logging() 

219 

220 # usedb flag 

221 self.usedb = usedb 

222 if self.usedb is None: 

223 self.usedb = self.get_bool(f'{self.appname}.config.usedb', 

224 default=False, usedb=False) 

225 

226 # preferdb flag 

227 self.preferdb = preferdb 

228 if self.usedb and self.preferdb is None: 

229 self.preferdb = self.get_bool(f'{self.appname}.config.preferdb', 

230 default=False, usedb=False) 

231 

232 # configure main app DB if applicable, or disable usedb flag 

233 try: 

234 from wuttjamaican.db import Session, get_engines 

235 except ImportError: 

236 if self.usedb: 

237 log.warning("config created with `usedb = True`, but can't import " 

238 "DB module(s), so setting `usedb = False` instead", 

239 exc_info=True) 

240 self.usedb = False 

241 self.preferdb = False 

242 else: 

243 self.appdb_engines = get_engines(self, f'{self.appname}.db') 

244 self.appdb_engine = self.appdb_engines.get('default') 

245 Session.configure(bind=self.appdb_engine) 

246 

247 log.debug("config files read: %s", self.files_read) 

248 

249 def _load_ini_configs(self, path, configs, require=True): 

250 path = os.path.abspath(path) 

251 

252 # try to load config from the given path 

253 try: 

254 config = configuration.config_from_ini(path, read_from_file=True) 

255 except FileNotFoundError: 

256 if not require: 

257 log.warning("INI config file not found: %s", path) 

258 return 

259 raise 

260 

261 # ok add that one to the mix 

262 configs.append(config) 

263 self.files_read.append(path) 

264 

265 # need parent folder of that path, for %(here)s interpolation 

266 here = os.path.dirname(path) 

267 

268 # bring in any "required" files 

269 requires = config.get(f'{self.appname}.config.require') 

270 if requires: 

271 for path in parse_list(requires): 

272 path = path % {'here': here} 

273 self._load_ini_configs(path, configs, require=True) 

274 

275 # bring in any "included" files 

276 includes = config.get(f'{self.appname}.config.include') 

277 if includes: 

278 for path in parse_list(includes): 

279 path = path % {'here': here} 

280 self._load_ini_configs(path, configs, require=False) 

281 

282 def get_prioritized_files(self): 

283 """ 

284 Returns list of config files in order of priority. 

285 

286 By default, :attr:`files_read` should already be in the 

287 correct order, but this is to make things more explicit. 

288 """ 

289 return self.files_read 

290 

291 def setdefault( 

292 self, 

293 key, 

294 value): 

295 """ 

296 Establish a default config value for the given key. 

297 

298 Note that there is only *one* default value per key. If 

299 multiple calls are made with the same key, the first will set 

300 the default and subsequent calls have no effect. 

301 

302 :returns: The current config value, *outside of the DB*. For 

303 various reasons this method may not be able to lookup 

304 settings from the DB, e.g. during app init. So it can only 

305 determine the value per INI files + config defaults. 

306 """ 

307 # set default value, if not already set 

308 self.defaults.setdefault(key, value) 

309 

310 # get current value, sans db 

311 return self.get(key, usedb=False) 

312 

313 def get( 

314 self, 

315 key, 

316 default=UNSPECIFIED, 

317 require=False, 

318 ignore_ambiguous=False, 

319 message=None, 

320 usedb=None, 

321 preferdb=None, 

322 session=None, 

323 ): 

324 """ 

325 Retrieve a string value from config. 

326 

327 .. warning:: 

328 

329 While the point of this method is to return a *string* 

330 value, it is possible for a key to be present in config 

331 which corresponds to a "subset" of the config, and not a 

332 simple value. For instance with this config file: 

333 

334 .. code-block:: ini 

335 

336 [foo] 

337 bar = 1 

338 bar.baz = 2 

339 

340 If you invoke ``config.get('foo.bar')`` the return value 

341 is somewhat ambiguous. At first glance it should return 

342 ``'1'`` - but just as valid would be to return the dict:: 

343 

344 {'baz': '2'} 

345 

346 And similarly, if you invoke ``config.get('foo')`` then 

347 the return value "should be" the dict:: 

348 

349 {'bar': '1', 

350 'bar.baz': '2'} 

351 

352 Despite all that ambiguity, again the whole point of this 

353 method is to return a *string* value, only. Therefore in 

354 any case where the return value "should be" a dict, per 

355 logic described above, this method will *ignore* that and 

356 simply return ``None`` (or rather the ``default`` value). 

357 

358 It is important also to understand that in fact, there is 

359 no "real" ambiguity per se, but rather a dict (subset) 

360 would always get priority over a simple string value. So 

361 in the first example above, ``config.get('foo.bar')`` will 

362 always return the ``default`` value. The string value 

363 ``'1'`` will never be returned since the dict/subset 

364 overshadows it, and this method will only return the 

365 default value in lieu of any dict. 

366 

367 :param key: String key for which value should be returned. 

368 

369 :param default: Default value to be returned, if config does 

370 not contain the key. If no default is specified, ``None`` 

371 will be assumed. 

372 

373 :param require: If set, an error will be raised if config does 

374 not contain the key. If not set, default value is returned 

375 (which may be ``None``). 

376 

377 Note that it is an error to specify a default value if you 

378 also specify ``require=True``. 

379 

380 :param ignore_ambiguous: By default this method will log a 

381 warning if an ambiguous value is detected (as described 

382 above). Pass a true value for this flag to avoid the 

383 warnings. Should use with caution, as the warnings are 

384 there for a reason. 

385 

386 :param message: Optional first part of message to be used, 

387 when raising a "value not found" error. If not specified, 

388 a default error message will be generated. 

389 

390 :param usedb: Flag indicating whether config values should be 

391 looked up from the DB. The default for this param is 

392 ``None``, in which case the :attr:`usedb` flag determines 

393 the behavior. 

394 

395 :param preferdb: Flag indicating whether config values from DB 

396 should be preferred over values from INI files and/or app 

397 defaults. The default for this param is ``None``, in which 

398 case the :attr:`preferdb` flag determines the behavior. 

399 

400 :param session: Optional SQLAlchemy session to use for DB lookups. 

401 NOTE: This param is not yet implemented; currently ignored. 

402 

403 :returns: Value as string. 

404 

405 """ 

406 if require and default is not UNSPECIFIED: 

407 raise ValueError("must not specify default value when require=True") 

408 

409 # should we use/prefer db? 

410 if usedb is None: 

411 usedb = self.usedb 

412 if usedb and preferdb is None: 

413 preferdb = self.preferdb 

414 

415 # read from db first if so requested 

416 if usedb and preferdb: 

417 value = self.get_from_db(key, session=session) 

418 if value is not None: 

419 return value 

420 

421 # read from defaults + INI files 

422 value = self.configuration.get(key) 

423 if value is not None: 

424 

425 # nb. if the "value" corresponding to the given key is in 

426 # fact a subset/dict of more config values, then we must 

427 # "ignore" that. so only return the value if it is *not* 

428 # such a config subset. 

429 if not isinstance(value, configuration.Configuration): 

430 return value 

431 

432 if not ignore_ambiguous: 

433 log.warning("ambiguous config key '%s' returns: %s", key, value) 

434 

435 # read from db last if so requested 

436 if usedb and not preferdb: 

437 value = self.get_from_db(key, session=session) 

438 if value is not None: 

439 return value 

440 

441 # raise error if required value not found 

442 if require: 

443 message = message or "missing config" 

444 raise ConfigurationError(f"{message}; set value for: {key}") 

445 

446 # give the default value if specified 

447 if default is not UNSPECIFIED: 

448 return default 

449 

450 def get_from_db(self, key, session=None): 

451 """ 

452 Retrieve a config value from database settings table. 

453 

454 This is a convenience wrapper around 

455 :meth:`~wuttjamaican.app.AppHandler.get_setting()`. 

456 """ 

457 app = self.get_app() 

458 with app.short_session(session=session) as s: 

459 return app.get_setting(s, key) 

460 

461 def require(self, *args, **kwargs): 

462 """ 

463 Retrieve a value from config, or raise error if no value can 

464 be found. This is just a shortcut, so these work the same:: 

465 

466 config.get('foo', require=True) 

467 

468 config.require('foo') 

469 """ 

470 kwargs['require'] = True 

471 return self.get(*args, **kwargs) 

472 

473 def get_bool(self, *args, **kwargs): 

474 """ 

475 Retrieve a boolean value from config. 

476 

477 Accepts same params as :meth:`get()` but if a value is found, 

478 it will be coerced to boolean via 

479 :func:`~wuttjamaican.util.parse_bool()`. 

480 """ 

481 value = self.get(*args, **kwargs) 

482 return parse_bool(value) 

483 

484 def get_int(self, *args, **kwargs): 

485 """ 

486 Retrieve an integer value from config. 

487 

488 Accepts same params as :meth:`get()` but if a value is found, 

489 it will be coerced to integer via the :class:`python:int()` 

490 constructor. 

491 """ 

492 value = self.get(*args, **kwargs) 

493 if value is not None: 

494 return int(value) 

495 

496 def get_list(self, *args, **kwargs): 

497 """ 

498 Retrieve a list value from config. 

499 

500 Accepts same params as :meth:`get()` but if a value is found, 

501 it will be coerced to list via 

502 :func:`~wuttjamaican.util.parse_list()`. 

503 

504 :returns: If a value is found, a list is returned. If no 

505 value, returns ``None``. 

506 """ 

507 value = self.get(*args, **kwargs) 

508 if value is not None: 

509 return parse_list(value) 

510 

511 def get_dict(self, prefix): 

512 """ 

513 Retrieve a particular group of values, as a dictionary. 

514 

515 Please note, this will only return values from INI files + 

516 defaults. It will *not* return values from DB settings. In 

517 other words it assumes ``usedb=False``. 

518 

519 For example given this config file: 

520 

521 .. code-block:: ini 

522 

523 [wutta.db] 

524 keys = default, host 

525 default.url = sqlite:///tmp/default.sqlite 

526 host.url = sqlite:///tmp/host.sqlite 

527 host.pool_pre_ping = true 

528 

529 One can get the "dict" for SQLAlchemy engine config via:: 

530 

531 config.get_dict('wutta.db') 

532 

533 And the dict would look like:: 

534 

535 {'keys': 'default, host', 

536 'default.url': 'sqlite:///tmp/default.sqlite', 

537 'host.url': 'sqlite:///tmp/host.sqlite', 

538 'host.pool_pre_ping': 'true'} 

539 

540 :param prefix: String prefix corresponding to a subsection of 

541 the config. 

542 

543 :returns: Dictionary containing the config subsection. 

544 """ 

545 try: 

546 values = self.configuration[prefix] 

547 except KeyError: 

548 return {} 

549 

550 return values.as_dict() 

551 

552 def _configure_logging(self): 

553 """ 

554 This will save the current config parser defaults to a 

555 temporary file, and use this file to configure Python's 

556 standard logging module. 

557 """ 

558 # write current values to file suitable for logging auto-config 

559 path = self._write_logging_config_file() 

560 try: 

561 logging.config.fileConfig(path, disable_existing_loggers=False) 

562 except configparser.NoSectionError as error: 

563 log.warning("tried to configure logging, but got NoSectionError: %s", error) 

564 else: 

565 log.debug("configured logging") 

566 log.debug("sys.argv: %s", sys.argv) 

567 finally: 

568 os.remove(path) 

569 

570 def _write_logging_config_file(self): 

571 

572 # load all current values into configparser 

573 parser = configparser.RawConfigParser() 

574 for section, values in self.configuration.items(): 

575 parser.add_section(section) 

576 for option, value in values.items(): 

577 parser.set(section, option, value) 

578 

579 # write INI file and return path 

580 fd, path = tempfile.mkstemp(suffix='.conf') 

581 os.close(fd) 

582 with open(path, 'wt') as f: 

583 parser.write(f) 

584 return path 

585 

586 def get_app(self): 

587 """ 

588 Returns the global :class:`~wuttjamaican.app.AppHandler` 

589 instance, creating it if necessary. 

590 

591 See also :doc:`/narr/handlers/app`. 

592 """ 

593 if not hasattr(self, '_app'): 

594 spec = self.get(f'{self.appname}.app.handler', usedb=False, 

595 default=self.default_app_handler_spec) 

596 factory = load_object(spec) 

597 self._app = factory(self) 

598 return self._app 

599 

600 def get_engine_maker(self): 

601 """ 

602 Returns a callable to be used for constructing SQLAlchemy 

603 engines fromc config. 

604 

605 Which callable is used depends on 

606 :attr:`default_engine_maker_spec` but by default will be 

607 :func:`wuttjamaican.db.conf.make_engine_from_config()`. 

608 """ 

609 return load_object(self.default_engine_maker_spec) 

610 

611 def production(self): 

612 """ 

613 Returns boolean indicating whether the app is running in 

614 production mode. 

615 

616 This value may be set e.g. in config file: 

617 

618 .. code-block:: ini 

619 

620 [wutta] 

621 production = true 

622 """ 

623 return self.get_bool(f'{self.appname}.production', default=False) 

624 

625 

626class WuttaConfigExtension: 

627 """ 

628 Base class for all :term:`config extensions <config extension>`. 

629 """ 

630 key = None 

631 

632 def __repr__(self): 

633 return f"WuttaConfigExtension(key={self.key})" 

634 

635 def configure(self, config): 

636 """ 

637 Subclass should override this method, to extend the config 

638 object in any way necessary. 

639 """ 

640 

641 def startup(self, config): 

642 """ 

643 This method is called after the config object is fully created 

644 and all extensions have been applied, i.e. after 

645 :meth:`configure()` has been called for each extension. 

646 

647 At this point the config *settings* for the running app should 

648 be settled, and each extension is then allowed to act on those 

649 initial settings if needed. 

650 """ 

651 

652 

653def generic_default_files(appname): 

654 """ 

655 Returns a list of default file paths which might be used for 

656 making a config object. This function does not check if the paths 

657 actually exist. 

658 

659 :param appname: App name to be used as basis for default filenames. 

660 

661 :returns: List of default file paths. 

662 """ 

663 if sys.platform == 'win32': 

664 # use pywin32 to fetch official defaults 

665 try: 

666 from win32com.shell import shell, shellcon 

667 except ImportError: 

668 return [] 

669 

670 return [ 

671 # e.g. C:\..?? TODO: what is the user-specific path on win32? 

672 os.path.join(shell.SHGetSpecialFolderPath( 

673 0, shellcon.CSIDL_APPDATA), appname, f'{appname}.conf'), 

674 os.path.join(shell.SHGetSpecialFolderPath( 

675 0, shellcon.CSIDL_APPDATA), f'{appname}.conf'), 

676 

677 # e.g. C:\ProgramData\wutta\wutta.conf 

678 os.path.join(shell.SHGetSpecialFolderPath( 

679 0, shellcon.CSIDL_COMMON_APPDATA), appname, f'{appname}.conf'), 

680 os.path.join(shell.SHGetSpecialFolderPath( 

681 0, shellcon.CSIDL_COMMON_APPDATA), f'{appname}.conf'), 

682 ] 

683 

684 # default paths for *nix 

685 return [ 

686 f'{sys.prefix}/app/{appname}.conf', 

687 

688 os.path.expanduser(f'~/.{appname}/{appname}.conf'), 

689 os.path.expanduser(f'~/.{appname}.conf'), 

690 

691 f'/usr/local/etc/{appname}/{appname}.conf', 

692 f'/usr/local/etc/{appname}.conf', 

693 

694 f'/etc/{appname}/{appname}.conf', 

695 f'/etc/{appname}.conf', 

696 ] 

697 

698 

699def get_config_paths( 

700 files=None, 

701 plus_files=None, 

702 appname='wutta', 

703 env_files_name=None, 

704 env_plus_files_name=None, 

705 env=None, 

706 default_files=None, 

707 winsvc=None): 

708 """ 

709 This function determines which files should ultimately be provided 

710 to the config constructor. It is normally called by 

711 :func:`make_config()`. 

712 

713 In short, the files to be used are determined by typical priority: 

714 

715 * function params - ``files`` and ``plus_files`` 

716 * environment variables - e.g. ``WUTTA_CONFIG_FILES`` 

717 * app defaults - e.g. :func:`generic_default_files()` 

718 

719 The "main" and so-called "plus" config files are dealt with 

720 separately, so that "defaults" can be used for the main files, and 

721 any "plus" files are then added to the result. 

722 

723 In the end it combines everything it finds into a single list. 

724 Note that it does not necessarily check to see if these files 

725 exist. 

726 

727 :param files: Explicit set of "main" config files. If not 

728 specified, environment variables and/or default lookup will be 

729 done to get the "main" file set. Specify an empty list to 

730 force an empty main file set. 

731 

732 :param plus_files: Explicit set of "plus" config files. Same 

733 rules apply here as for the ``files`` param. 

734 

735 :param appname: The "app name" to use as basis for other things - 

736 namely, constructing the default config file paths etc. For 

737 instance the default ``appname`` value is ``'wutta'`` which 

738 leads to default env vars like ``WUTTA_CONFIG_FILES``. 

739 

740 :param env_files_name: Name of the environment variable to read, 

741 if ``files`` is not specified. The default is 

742 ``WUTTA_CONFIG_FILES`` unless you override ``appname``. 

743 

744 :param env_plus_files_name: Name of the environment variable to 

745 read, if ``plus_files`` is not specified. The default is 

746 ``WUTTA_CONFIG_PLUS_FILES`` unless you override ``appname``. 

747 

748 :param env: Optional environment dict; if not specified 

749 ``os.environ`` is used. 

750 

751 :param default_files: Optional lookup for "default" file paths. 

752 

753 This is only used a) for the "main" config file lookup (but not 

754 "plus" files), and b) if neither ``files`` nor the environment 

755 variables yielded anything. 

756 

757 If not specified, :func:`generic_default_files()` will be used 

758 for the lookup. 

759 

760 You may specify a single file path as string, or a list of file 

761 paths, or a callable which returns either of those things. For 

762 example any of these could be used:: 

763 

764 mydefaults = '/tmp/something.conf' 

765 

766 mydefaults = [ 

767 '/tmp/something.conf', 

768 '/tmp/else.conf', 

769 ] 

770 

771 def mydefaults(appname): 

772 return [ 

773 f"/tmp/{appname}.conf", 

774 f"/tmp/{appname}.ini", 

775 ] 

776 

777 files = get_config_paths(default_files=mydefaults) 

778 

779 :param winsvc: Optional internal name of the Windows service for 

780 which the config object is being made. 

781 

782 This is only needed for true Windows services running via 

783 "Python for Windows Extensions" - which probably only includes 

784 the Rattail File Monitor service. 

785 

786 In this context there is no way to tell the app which config 

787 files to read on startup, so it can only look for "default" 

788 files. But by passing a ``winsvc`` name to this function, it 

789 will first load the default config file, then read a particular 

790 value to determine the "real" config file(s) it should use. 

791 

792 So for example on Windows you might have a config file at 

793 ``C:\\ProgramData\\rattail\\rattail.conf`` with contents: 

794 

795 .. code-block:: ini 

796 

797 [rattail.config] 

798 winsvc.RattailFileMonitor = C:\\ProgramData\\rattail\\filemon.conf 

799 

800 And then ``C:\\ProgramData\\rattail\\filemon.conf`` would have 

801 the actual config for the filemon service. 

802 

803 When the service starts it calls:: 

804 

805 make_config(winsvc='RattailFileMonitor') 

806 

807 which first reads the ``rattail.conf`` file (since that is the 

808 only sensible default), but then per config it knows to swap 

809 that out for ``filemon.conf`` at startup. This is because it 

810 finds a config value matching the requested service name. The 

811 end result is as if it called this instead:: 

812 

813 make_config(files=[r'C:\\ProgramData\\rattail\\filemon.conf']) 

814 

815 :returns: List of file paths. 

816 """ 

817 if env is None: 

818 env = os.environ 

819 

820 # first identify any "primary" config files 

821 if files is None: 

822 if not env_files_name: 

823 env_files_name = f'{appname.upper()}_CONFIG_FILES' 

824 

825 files = env.get(env_files_name) 

826 if files is not None: 

827 files = files.split(os.pathsep) 

828 

829 elif default_files: 

830 if callable(default_files): 

831 files = default_files(appname) or [] 

832 elif isinstance(default_files, str): 

833 files = [default_files] 

834 else: 

835 files = list(default_files) 

836 files = [path for path in files 

837 if os.path.exists(path)] 

838 

839 else: 

840 files = [] 

841 for path in generic_default_files(appname): 

842 if os.path.exists(path): 

843 files.append(path) 

844 

845 elif isinstance(files, str): 

846 files = [files] 

847 else: 

848 files = list(files) 

849 

850 # then identify any "plus" (config tweak) files 

851 if plus_files is None: 

852 if not env_plus_files_name: 

853 env_plus_files_name = f'{appname.upper()}_CONFIG_PLUS_FILES' 

854 

855 plus_files = env.get(env_plus_files_name) 

856 if plus_files is not None: 

857 plus_files = plus_files.split(os.pathsep) 

858 

859 else: 

860 plus_files = [] 

861 

862 elif isinstance(plus_files, str): 

863 plus_files = [plus_files] 

864 else: 

865 plus_files = list(plus_files) 

866 

867 # combine all files 

868 files.extend(plus_files) 

869 

870 # when running as a proper windows service, must first read 

871 # "default" file(s) and then consult config to see which file 

872 # should "really" be used. because there isn't a way to specify 

873 # which config file as part of the actual service definition in 

874 # windows, so the service name is used for magic lookup here. 

875 if winsvc: 

876 config = configparser.ConfigParser() 

877 config.read(files) 

878 section = f'{appname}.config' 

879 if config.has_section(section): 

880 option = f'winsvc.{winsvc}' 

881 if config.has_option(section, option): 

882 # replace file paths with whatever config value says 

883 files = parse_list(config.get(section, option)) 

884 

885 return files 

886 

887 

888def make_config( 

889 files=None, 

890 plus_files=None, 

891 appname='wutta', 

892 env_files_name=None, 

893 env_plus_files_name=None, 

894 env=None, 

895 default_files=None, 

896 winsvc=None, 

897 usedb=None, 

898 preferdb=None, 

899 factory=None, 

900 extend=True, 

901 extension_entry_points=None, 

902 **kwargs): 

903 """ 

904 Make a new config (usually :class:`WuttaConfig`) object, 

905 initialized per the given parameters and (usually) further 

906 modified by all registered config extensions. 

907 

908 This function really does 3 things: 

909 

910 * determine the set of config files to use 

911 * pass those files to config factory 

912 * apply extensions to the resulting config object 

913 

914 Some params are described in :func:`get_config_paths()` since they 

915 are passed as-is to that function for the first step. 

916 

917 :param appname: The :term:`app name` to use as basis for other 

918 things - namely, it affects how config files are located. This 

919 name is also passed to the config factory at which point it 

920 becomes :attr:`~wuttjamaican.conf.WuttaConfig.appname`. 

921 

922 :param usedb: Passed to the config factory; becomes 

923 :attr:`~wuttjamaican.conf.WuttaConfig.usedb`. 

924 

925 :param preferdb: Passed to the config factory; becomes 

926 :attr:`~wuttjamaican.conf.WuttaConfig.preferdb`. 

927 

928 :param factory: Optional factory to use when making the object. 

929 Default factory is :class:`WuttaConfig`. 

930 

931 :param extend: Whether to "auto-extend" the config with all 

932 registered extensions. 

933 

934 As a general rule, ``make_config()`` should only be called 

935 once, upon app startup. This is because some of the config 

936 extensions may do things which should only happen one time. 

937 However if ``extend=False`` is specified, then no extensions 

938 are invoked, so this may be done multiple times. 

939 

940 (Why anyone would need this, is another question..maybe only 

941 useful for tests.) 

942 

943 :param extension_entry_points: Name of the ``setuptools`` entry 

944 points section, used to identify registered config extensions. 

945 The default is ``wutta.config.extensions`` unless you override 

946 ``appname``. 

947 

948 :returns: The new config object. 

949 """ 

950 # collect file paths 

951 files = get_config_paths( 

952 files=files, 

953 plus_files=plus_files, 

954 appname=appname, 

955 env_files_name=env_files_name, 

956 env_plus_files_name=env_plus_files_name, 

957 env=env, 

958 default_files=default_files, 

959 winsvc=winsvc) 

960 

961 # make config object 

962 if not factory: 

963 factory = WuttaConfig 

964 config = factory(files, appname=appname, 

965 usedb=usedb, preferdb=preferdb, 

966 **kwargs) 

967 

968 # maybe extend config object 

969 if extend: 

970 if not extension_entry_points: 

971 extension_entry_points = f'{appname}.config.extensions' 

972 

973 # apply all registered extensions 

974 # TODO: maybe let config disable some extensions? 

975 extensions = load_entry_points(extension_entry_points) 

976 extensions = [ext() for ext in extensions.values()] 

977 for extension in extensions: 

978 log.debug("applying config extension: %s", extension.key) 

979 extension.configure(config) 

980 

981 # let extensions run startup hooks if needed 

982 for extension in extensions: 

983 extension.startup(config) 

984 

985 return config