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

238 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-15 11:33 -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 

205 # add config for use w/ setdefault() 

206 self.defaults = configuration.Configuration(defaults) 

207 configs.append(self.defaults) 

208 

209 # master config set 

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

211 

212 # establish logging 

213 if configure_logging is None: 

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

215 default=False, usedb=False) 

216 if configure_logging: 

217 self._configure_logging() 

218 

219 # usedb flag 

220 self.usedb = usedb 

221 if self.usedb is None: 

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

223 default=False, usedb=False) 

224 

225 # preferdb flag 

226 self.preferdb = preferdb 

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

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

229 default=False, usedb=False) 

230 

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

232 try: 

233 from wuttjamaican.db import Session, get_engines 

234 except ImportError: 

235 if self.usedb: 

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

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

238 exc_info=True) 

239 self.usedb = False 

240 self.preferdb = False 

241 else: 

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

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

244 Session.configure(bind=self.appdb_engine) 

245 

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

247 

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

249 path = os.path.abspath(path) 

250 

251 # no need to read a file twice; its first appearance sets priority 

252 if path in self.files_read: 

253 return 

254 

255 # try to load config with standard parser, and default vars 

256 here = os.path.dirname(path) 

257 config = configparser.ConfigParser(defaults={'here': here, '__file__': path}) 

258 if not config.read(path): 

259 if require: 

260 raise FileNotFoundError(f"could not read required config file: {path}") 

261 return 

262 

263 # load all values into (yet another) temp config 

264 temp_config = configparser.RawConfigParser() 

265 for section in config.sections(): 

266 temp_config.add_section(section) 

267 # nb. must interpolate most values but *not* for logging formatters 

268 raw = section.startswith('formatter_') 

269 for option in config.options(section): 

270 temp_config.set(section, option, config.get(section, option, raw=raw)) 

271 

272 # re-write as temp file with "final" values 

273 fd, temp_path = tempfile.mkstemp(suffix='.ini') 

274 os.close(fd) 

275 with open(temp_path, 'wt') as f: 

276 temp_config.write(f) 

277 

278 # and finally, load that into our main config 

279 config = configuration.config_from_ini(temp_path, read_from_file=True) 

280 configs.append(config) 

281 self.files_read.append(path) 

282 os.remove(temp_path) 

283 

284 # bring in any "required" files 

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

286 if requires: 

287 for path in self.parse_list(requires): 

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

289 

290 # bring in any "included" files 

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

292 if includes: 

293 for path in self.parse_list(includes): 

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

295 

296 def get_prioritized_files(self): 

297 """ 

298 Returns list of config files in order of priority. 

299 

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

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

302 """ 

303 return self.files_read 

304 

305 def setdefault( 

306 self, 

307 key, 

308 value): 

309 """ 

310 Establish a default config value for the given key. 

311 

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

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

314 the default and subsequent calls have no effect. 

315 

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

317 various reasons this method may not be able to lookup 

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

319 determine the value per INI files + config defaults. 

320 """ 

321 # set default value, if not already set 

322 self.defaults.setdefault(key, value) 

323 

324 # get current value, sans db 

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

326 

327 def get( 

328 self, 

329 key, 

330 default=UNSPECIFIED, 

331 require=False, 

332 ignore_ambiguous=False, 

333 message=None, 

334 usedb=None, 

335 preferdb=None, 

336 session=None, 

337 ): 

338 """ 

339 Retrieve a string value from config. 

340 

341 .. warning:: 

342 

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

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

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

346 simple value. For instance with this config file: 

347 

348 .. code-block:: ini 

349 

350 [foo] 

351 bar = 1 

352 bar.baz = 2 

353 

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

355 is somewhat ambiguous. At first glance it should return 

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

357 

358 {'baz': '2'} 

359 

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

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

362 

363 {'bar': '1', 

364 'bar.baz': '2'} 

365 

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

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

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

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

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

371 

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

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

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

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

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

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

378 overshadows it, and this method will only return the 

379 default value in lieu of any dict. 

380 

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

382 

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

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

385 will be assumed. 

386 

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

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

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

390 

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

392 also specify ``require=True``. 

393 

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

395 warning if an ambiguous value is detected (as described 

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

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

398 there for a reason. 

399 

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

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

402 a default error message will be generated. 

403 

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

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

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

407 the behavior. 

408 

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

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

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

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

413 

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

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

416 

417 :returns: Value as string. 

418 

419 """ 

420 if require and default is not UNSPECIFIED: 

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

422 

423 # should we use/prefer db? 

424 if usedb is None: 

425 usedb = self.usedb 

426 if usedb and preferdb is None: 

427 preferdb = self.preferdb 

428 

429 # read from db first if so requested 

430 if usedb and preferdb: 

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

432 if value is not None: 

433 return value 

434 

435 # read from defaults + INI files 

436 value = self.configuration.get(key) 

437 if value is not None: 

438 

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

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

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

442 # such a config subset. 

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

444 return value 

445 

446 if not ignore_ambiguous: 

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

448 

449 # read from db last if so requested 

450 if usedb and not preferdb: 

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

452 if value is not None: 

453 return value 

454 

455 # raise error if required value not found 

456 if require: 

457 message = message or "missing config" 

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

459 

460 # give the default value if specified 

461 if default is not UNSPECIFIED: 

462 return default 

463 

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

465 """ 

466 Retrieve a config value from database settings table. 

467 

468 This is a convenience wrapper around 

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

470 """ 

471 app = self.get_app() 

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

473 return app.get_setting(s, key) 

474 

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

476 """ 

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

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

479 

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

481 

482 config.require('foo') 

483 """ 

484 kwargs['require'] = True 

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

486 

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

488 """ 

489 Retrieve a boolean value from config. 

490 

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

492 it will be coerced to boolean via :meth:`parse_bool()`. 

493 """ 

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

495 return self.parse_bool(value) 

496 

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

498 """ 

499 Retrieve an integer value from config. 

500 

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

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

503 constructor. 

504 """ 

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

506 if value is not None: 

507 return int(value) 

508 

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

510 """ 

511 Retrieve a list value from config. 

512 

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

514 it will be coerced to list via :meth:`parse_list()`. 

515 

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

517 value, returns ``None``. 

518 """ 

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

520 if value is not None: 

521 return self.parse_list(value) 

522 

523 def get_dict(self, prefix): 

524 """ 

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

526 

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

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

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

530 

531 For example given this config file: 

532 

533 .. code-block:: ini 

534 

535 [wutta.db] 

536 keys = default, host 

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

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

539 host.pool_pre_ping = true 

540 

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

542 

543 config.get_dict('wutta.db') 

544 

545 And the dict would look like:: 

546 

547 {'keys': 'default, host', 

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

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

550 'host.pool_pre_ping': 'true'} 

551 

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

553 the config. 

554 

555 :returns: Dictionary containing the config subsection. 

556 """ 

557 try: 

558 values = self.configuration[prefix] 

559 except KeyError: 

560 return {} 

561 

562 return values.as_dict() 

563 

564 def parse_bool(self, value): 

565 """ 

566 Convenience wrapper for 

567 :func:`wuttjamaican.util.parse_bool()`. 

568 """ 

569 return parse_bool(value) 

570 

571 def parse_list(self, value): 

572 """ 

573 Convenience wrapper for 

574 :func:`wuttjamaican.util.parse_list()`. 

575 """ 

576 return parse_list(value) 

577 

578 def _configure_logging(self): 

579 """ 

580 This will save the current config parser defaults to a 

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

582 standard logging module. 

583 """ 

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

585 path = self._write_logging_config_file() 

586 try: 

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

588 except configparser.NoSectionError as error: 

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

590 else: 

591 log.debug("configured logging") 

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

593 finally: 

594 os.remove(path) 

595 

596 def _write_logging_config_file(self): 

597 

598 # load all current values into configparser 

599 parser = configparser.RawConfigParser() 

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

601 parser.add_section(section) 

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

603 parser.set(section, option, value) 

604 

605 # write INI file and return path 

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

607 os.close(fd) 

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

609 parser.write(f) 

610 return path 

611 

612 def get_app(self): 

613 """ 

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

615 instance, creating it if necessary. 

616 

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

618 """ 

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

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

621 default=self.default_app_handler_spec) 

622 factory = load_object(spec) 

623 self._app = factory(self) 

624 return self._app 

625 

626 def get_engine_maker(self): 

627 """ 

628 Returns a callable to be used for constructing SQLAlchemy 

629 engines fromc config. 

630 

631 Which callable is used depends on 

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

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

634 """ 

635 return load_object(self.default_engine_maker_spec) 

636 

637 def production(self): 

638 """ 

639 Returns boolean indicating whether the app is running in 

640 production mode. 

641 

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

643 

644 .. code-block:: ini 

645 

646 [wutta] 

647 production = true 

648 """ 

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

650 

651 

652class WuttaConfigExtension: 

653 """ 

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

655 """ 

656 key = None 

657 

658 def __repr__(self): 

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

660 

661 def configure(self, config): 

662 """ 

663 Subclass should override this method, to extend the config 

664 object in any way necessary. 

665 """ 

666 

667 def startup(self, config): 

668 """ 

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

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

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

672 

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

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

675 initial settings if needed. 

676 """ 

677 

678 

679def generic_default_files(appname): 

680 """ 

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

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

683 actually exist. 

684 

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

686 

687 :returns: List of default file paths. 

688 """ 

689 if sys.platform == 'win32': 

690 # use pywin32 to fetch official defaults 

691 try: 

692 from win32com.shell import shell, shellcon 

693 except ImportError: 

694 return [] 

695 

696 return [ 

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

698 os.path.join(shell.SHGetSpecialFolderPath( 

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

700 os.path.join(shell.SHGetSpecialFolderPath( 

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

702 

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

704 os.path.join(shell.SHGetSpecialFolderPath( 

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

706 os.path.join(shell.SHGetSpecialFolderPath( 

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

708 ] 

709 

710 # default paths for *nix 

711 return [ 

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

713 

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

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

716 

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

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

719 

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

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

722 ] 

723 

724 

725def get_config_paths( 

726 files=None, 

727 plus_files=None, 

728 appname='wutta', 

729 env_files_name=None, 

730 env_plus_files_name=None, 

731 env=None, 

732 default_files=None, 

733 winsvc=None): 

734 """ 

735 This function determines which files should ultimately be provided 

736 to the config constructor. It is normally called by 

737 :func:`make_config()`. 

738 

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

740 

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

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

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

744 

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

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

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

748 

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

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

751 exist. 

752 

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

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

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

756 force an empty main file set. 

757 

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

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

760 

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

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

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

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

765 

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

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

768 ``WUTTA_CONFIG_FILES`` unless you override ``appname``. 

769 

770 :param env_plus_files_name: Name of the environment variable to 

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

772 ``WUTTA_CONFIG_PLUS_FILES`` unless you override ``appname``. 

773 

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

775 ``os.environ`` is used. 

776 

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

778 

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

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

781 variables yielded anything. 

782 

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

784 for the lookup. 

785 

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

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

788 example any of these could be used:: 

789 

790 mydefaults = '/tmp/something.conf' 

791 

792 mydefaults = [ 

793 '/tmp/something.conf', 

794 '/tmp/else.conf', 

795 ] 

796 

797 def mydefaults(appname): 

798 return [ 

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

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

801 ] 

802 

803 files = get_config_paths(default_files=mydefaults) 

804 

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

806 which the config object is being made. 

807 

808 This is only needed for true Windows services running via 

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

810 the Rattail File Monitor service. 

811 

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

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

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

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

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

817 

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

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

820 

821 .. code-block:: ini 

822 

823 [rattail.config] 

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

825 

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

827 the actual config for the filemon service. 

828 

829 When the service starts it calls:: 

830 

831 make_config(winsvc='RattailFileMonitor') 

832 

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

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

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

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

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

838 

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

840 

841 :returns: List of file paths. 

842 """ 

843 if env is None: 

844 env = os.environ 

845 

846 # first identify any "primary" config files 

847 if files is None: 

848 if not env_files_name: 

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

850 

851 files = env.get(env_files_name) 

852 if files is not None: 

853 files = files.split(os.pathsep) 

854 

855 elif default_files: 

856 if callable(default_files): 

857 files = default_files(appname) or [] 

858 elif isinstance(default_files, str): 

859 files = [default_files] 

860 else: 

861 files = list(default_files) 

862 files = [path for path in files 

863 if os.path.exists(path)] 

864 

865 else: 

866 files = [] 

867 for path in generic_default_files(appname): 

868 if os.path.exists(path): 

869 files.append(path) 

870 

871 elif isinstance(files, str): 

872 files = [files] 

873 else: 

874 files = list(files) 

875 

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

877 if plus_files is None: 

878 if not env_plus_files_name: 

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

880 

881 plus_files = env.get(env_plus_files_name) 

882 if plus_files is not None: 

883 plus_files = plus_files.split(os.pathsep) 

884 

885 else: 

886 plus_files = [] 

887 

888 elif isinstance(plus_files, str): 

889 plus_files = [plus_files] 

890 else: 

891 plus_files = list(plus_files) 

892 

893 # combine all files 

894 files.extend(plus_files) 

895 

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

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

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

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

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

901 if winsvc: 

902 config = configparser.ConfigParser() 

903 config.read(files) 

904 section = f'{appname}.config' 

905 if config.has_section(section): 

906 option = f'winsvc.{winsvc}' 

907 if config.has_option(section, option): 

908 # replace file paths with whatever config value says 

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

910 

911 return files 

912 

913 

914def make_config( 

915 files=None, 

916 plus_files=None, 

917 appname='wutta', 

918 env_files_name=None, 

919 env_plus_files_name=None, 

920 env=None, 

921 default_files=None, 

922 winsvc=None, 

923 usedb=None, 

924 preferdb=None, 

925 factory=None, 

926 extend=True, 

927 extension_entry_points=None, 

928 **kwargs): 

929 """ 

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

931 initialized per the given parameters and (usually) further 

932 modified by all registered config extensions. 

933 

934 This function really does 3 things: 

935 

936 * determine the set of config files to use 

937 * pass those files to config factory 

938 * apply extensions to the resulting config object 

939 

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

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

942 

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

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

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

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

947 

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

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

950 

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

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

953 

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

955 Default factory is :class:`WuttaConfig`. 

956 

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

958 registered extensions. 

959 

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

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

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

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

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

965 

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

967 useful for tests.) 

968 

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

970 points section, used to identify registered config extensions. 

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

972 ``appname``. 

973 

974 :returns: The new config object. 

975 """ 

976 # collect file paths 

977 files = get_config_paths( 

978 files=files, 

979 plus_files=plus_files, 

980 appname=appname, 

981 env_files_name=env_files_name, 

982 env_plus_files_name=env_plus_files_name, 

983 env=env, 

984 default_files=default_files, 

985 winsvc=winsvc) 

986 

987 # make config object 

988 if not factory: 

989 factory = WuttaConfig 

990 config = factory(files, appname=appname, 

991 usedb=usedb, preferdb=preferdb, 

992 **kwargs) 

993 

994 # maybe extend config object 

995 if extend: 

996 if not extension_entry_points: 

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

998 

999 # apply all registered extensions 

1000 # TODO: maybe let config disable some extensions? 

1001 extensions = load_entry_points(extension_entry_points) 

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

1003 for extension in extensions: 

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

1005 extension.configure(config) 

1006 

1007 # let extensions run startup hooks if needed 

1008 for extension in extensions: 

1009 extension.startup(config) 

1010 

1011 return config