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

214 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-05-06 21:45 -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:: files_read 

130 

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

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

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

134 first file with the value wins. 

135 

136 .. attribute:: usedb 

137 

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

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

140 enabled via config file: 

141 

142 .. code-block:: ini 

143 

144 [wutta.config] 

145 usedb = true 

146 

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

148 

149 .. attribute:: preferdb 

150 

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

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

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

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

155 

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

157 settings table is updated, it will immediately affect app 

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

159 

160 .. code-block:: ini 

161 

162 [wutta.config] 

163 usedb = true 

164 preferdb = true 

165 

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

167 """ 

168 

169 def __init__( 

170 self, 

171 files=[], 

172 defaults={}, 

173 appname='wutta', 

174 usedb=None, 

175 preferdb=None, 

176 configure_logging=None, 

177 ): 

178 self.appname = appname 

179 configs = [] 

180 

181 # read all files requested 

182 self.files_read = [] 

183 for path in files: 

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

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

186 

187 # add config for use w/ setdefault() 

188 self.defaults = configuration.Configuration(defaults) 

189 configs.append(self.defaults) 

190 

191 # master config set 

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

193 

194 # establish logging 

195 if configure_logging is None: 

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

197 default=False, usedb=False) 

198 if configure_logging: 

199 self._configure_logging() 

200 

201 # usedb flag 

202 self.usedb = usedb 

203 if self.usedb is None: 

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

205 default=False, usedb=False) 

206 

207 # preferdb flag 

208 self.preferdb = preferdb 

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

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

211 default=False, usedb=False) 

212 

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

214 try: 

215 from .db import Session, get_engines 

216 except ImportError: 

217 if self.usedb: 

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

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

220 exc_info=True) 

221 self.usedb = False 

222 self.preferdb = False 

223 else: 

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

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

226 Session.configure(bind=self.appdb_engine) 

227 

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

229 

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

231 path = os.path.abspath(path) 

232 

233 # try to load config from the given path 

234 try: 

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

236 except FileNotFoundError: 

237 if not require: 

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

239 return 

240 raise 

241 

242 # ok add that one to the mix 

243 configs.append(config) 

244 self.files_read.append(path) 

245 

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

247 here = os.path.dirname(path) 

248 

249 # bring in any "required" files 

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

251 if requires: 

252 for path in parse_list(requires): 

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

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

255 

256 # bring in any "included" files 

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

258 if includes: 

259 for path in parse_list(includes): 

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

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

262 

263 def get_prioritized_files(self): 

264 """ 

265 Returns list of config files in order of priority. 

266 

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

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

269 """ 

270 return self.files_read 

271 

272 def setdefault( 

273 self, 

274 key, 

275 value): 

276 """ 

277 Establish a default config value for the given key. 

278 

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

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

281 the default and subsequent calls have no effect. 

282 

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

284 various reasons this method may not be able to lookup 

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

286 determine the value per INI files + config defaults. 

287 """ 

288 # set default value, if not already set 

289 self.defaults.setdefault(key, value) 

290 

291 # get current value, sans db 

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

293 

294 def get( 

295 self, 

296 key, 

297 default=UNSPECIFIED, 

298 require=False, 

299 ignore_ambiguous=False, 

300 message=None, 

301 usedb=None, 

302 preferdb=None, 

303 session=None, 

304 ): 

305 """ 

306 Retrieve a string value from config. 

307 

308 .. warning:: 

309 

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

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

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

313 simple value. For instance with this config file: 

314 

315 .. code-block:: ini 

316 

317 [foo] 

318 bar = 1 

319 bar.baz = 2 

320 

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

322 is somewhat ambiguous. At first glance it should return 

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

324 

325 {'baz': '2'} 

326 

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

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

329 

330 {'bar': '1', 

331 'bar.baz': '2'} 

332 

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

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

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

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

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

338 

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

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

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

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

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

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

345 overshadows it, and this method will only return the 

346 default value in lieu of any dict. 

347 

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

349 

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

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

352 will be assumed. 

353 

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

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

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

357 

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

359 also specify ``require=True``. 

360 

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

362 warning if an ambiguous value is detected (as described 

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

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

365 there for a reason. 

366 

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

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

369 a default error message will be generated. 

370 

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

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

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

374 the behavior. 

375 

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

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

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

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

380 

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

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

383 

384 :returns: Value as string. 

385 

386 """ 

387 if require and default is not UNSPECIFIED: 

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

389 

390 # should we use/prefer db? 

391 if usedb is None: 

392 usedb = self.usedb 

393 if usedb and preferdb is None: 

394 preferdb = self.preferdb 

395 

396 # read from db first if so requested 

397 if usedb and preferdb: 

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

399 if value is not None: 

400 return value 

401 

402 # read from defaults + INI files 

403 value = self.configuration.get(key) 

404 if value is not None: 

405 

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

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

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

409 # such a config subset. 

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

411 return value 

412 

413 if not ignore_ambiguous: 

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

415 

416 # read from db last if so requested 

417 if usedb and not preferdb: 

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

419 if value is not None: 

420 return value 

421 

422 # raise error if required value not found 

423 if require: 

424 message = message or "missing or invalid config" 

425 raise ConfigurationError(f"{message}; please set config value for: {key}") 

426 

427 # give the default value if specified 

428 if default is not UNSPECIFIED: 

429 return default 

430 

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

432 """ 

433 Retrieve a config value from database settings table. 

434 

435 This is a convenience wrapper around 

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

437 """ 

438 app = self.get_app() 

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

440 return app.get_setting(s, key) 

441 

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

443 """ 

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

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

446 

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

448 

449 config.require('foo') 

450 """ 

451 kwargs['require'] = True 

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

453 

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

455 """ 

456 Retrieve a boolean value from config. 

457 

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

459 it will be coerced to boolean via 

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

461 """ 

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

463 return parse_bool(value) 

464 

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

466 """ 

467 Retrieve an integer value from config. 

468 

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

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

471 constructor. 

472 """ 

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

474 if value is not None: 

475 return int(value) 

476 

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

478 """ 

479 Retrieve a list value from config. 

480 

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

482 it will be coerced to list via 

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

484 

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

486 value, returns ``None``. 

487 """ 

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

489 if value is not None: 

490 return parse_list(value) 

491 

492 def get_dict(self, prefix): 

493 """ 

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

495 

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

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

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

499 

500 For example given this config file: 

501 

502 .. code-block:: ini 

503 

504 [wutta.db] 

505 keys = default, host 

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

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

508 host.pool_pre_ping = true 

509 

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

511 

512 config.get_dict('wutta.db') 

513 

514 And the dict would look like:: 

515 

516 {'keys': 'default, host', 

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

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

519 'host.pool_pre_ping': 'true'} 

520 

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

522 the config. 

523 

524 :returns: Dictionary containing the config subsection. 

525 """ 

526 try: 

527 values = self.configuration[prefix] 

528 except KeyError: 

529 return {} 

530 

531 return values.as_dict() 

532 

533 def _configure_logging(self): 

534 """ 

535 This will save the current config parser defaults to a 

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

537 standard logging module. 

538 """ 

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

540 path = self._write_logging_config_file() 

541 try: 

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

543 except configparser.NoSectionError as error: 

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

545 else: 

546 log.debug("configured logging") 

547 finally: 

548 os.remove(path) 

549 

550 def _write_logging_config_file(self): 

551 

552 # load all current values into configparser 

553 parser = configparser.RawConfigParser() 

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

555 parser.add_section(section) 

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

557 parser.set(section, option, value) 

558 

559 # write INI file and return path 

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

561 os.close(fd) 

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

563 parser.write(f) 

564 return path 

565 

566 def get_app(self): 

567 """ 

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

569 instance, creating it if necessary. 

570 

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

572 """ 

573 if not hasattr(self, 'app'): 

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

575 default='wuttjamaican.app:AppHandler') 

576 factory = load_object(spec) 

577 self.app = factory(self) 

578 return self.app 

579 

580 

581class WuttaConfigExtension: 

582 """ 

583 Base class for all config extensions. 

584 """ 

585 key = None 

586 

587 def __repr__(self): 

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

589 

590 def configure(self, config): 

591 """ 

592 Subclass should override this method, to extend the config 

593 object in any way necessary. 

594 """ 

595 

596 

597def generic_default_files(appname): 

598 """ 

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

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

601 actually exist. 

602 

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

604 

605 :returns: List of default file paths. 

606 """ 

607 if sys.platform == 'win32': 

608 # use pywin32 to fetch official defaults 

609 try: 

610 from win32com.shell import shell, shellcon 

611 except ImportError: 

612 return [] 

613 

614 return [ 

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

616 os.path.join(shell.SHGetSpecialFolderPath( 

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

618 os.path.join(shell.SHGetSpecialFolderPath( 

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

620 

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

622 os.path.join(shell.SHGetSpecialFolderPath( 

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

624 os.path.join(shell.SHGetSpecialFolderPath( 

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

626 ] 

627 

628 # default paths for *nix 

629 return [ 

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

631 

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

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

634 

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

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

637 

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

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

640 ] 

641 

642 

643def get_config_paths( 

644 files=None, 

645 plus_files=None, 

646 appname='wutta', 

647 env_files_name=None, 

648 env_plus_files_name=None, 

649 env=None, 

650 default_files=None, 

651 winsvc=None): 

652 """ 

653 This function determines which files should ultimately be provided 

654 to the config constructor. It is normally called by 

655 :func:`make_config()`. 

656 

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

658 

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

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

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

662 

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

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

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

666 

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

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

669 exist. 

670 

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

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

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

674 force an empty main file set. 

675 

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

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

678 

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

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

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

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

683 

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

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

686 ``WUTTA_CONFIG_FILES`` unless you override ``appname``. 

687 

688 :param env_plus_files_name: Name of the environment variable to 

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

690 ``WUTTA_CONFIG_PLUS_FILES`` unless you override ``appname``. 

691 

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

693 ``os.environ`` is used. 

694 

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

696 

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

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

699 variables yielded anything. 

700 

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

702 for the lookup. 

703 

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

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

706 example any of these could be used:: 

707 

708 mydefaults = '/tmp/something.conf' 

709 

710 mydefaults = [ 

711 '/tmp/something.conf', 

712 '/tmp/else.conf', 

713 ] 

714 

715 def mydefaults(appname): 

716 return [ 

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

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

719 ] 

720 

721 files = get_config_paths(default_files=mydefaults) 

722 

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

724 which the config object is being made. 

725 

726 This is only needed for true Windows services running via 

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

728 the Rattail File Monitor service. 

729 

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

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

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

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

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

735 

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

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

738 

739 .. code-block:: ini 

740 

741 [rattail.config] 

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

743 

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

745 the actual config for the filemon service. 

746 

747 When the service starts it calls:: 

748 

749 make_config(winsvc='RattailFileMonitor') 

750 

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

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

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

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

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

756 

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

758 

759 :returns: List of file paths. 

760 """ 

761 if env is None: 

762 env = os.environ 

763 

764 # first identify any "primary" config files 

765 if files is None: 

766 if not env_files_name: 

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

768 

769 files = env.get(env_files_name) 

770 if files is not None: 

771 files = files.split(os.pathsep) 

772 

773 elif default_files: 

774 if callable(default_files): 

775 files = default_files(appname) or [] 

776 elif isinstance(default_files, str): 

777 files = [default_files] 

778 else: 

779 files = list(default_files) 

780 

781 else: 

782 files = [] 

783 for path in generic_default_files(appname): 

784 if os.path.exists(path): 

785 files.append(path) 

786 

787 elif isinstance(files, str): 

788 files = [files] 

789 else: 

790 files = list(files) 

791 

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

793 if plus_files is None: 

794 if not env_plus_files_name: 

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

796 

797 plus_files = env.get(env_plus_files_name) 

798 if plus_files is not None: 

799 plus_files = plus_files.split(os.pathsep) 

800 

801 else: 

802 plus_files = [] 

803 

804 elif isinstance(plus_files, str): 

805 plus_files = [plus_files] 

806 else: 

807 plus_files = list(plus_files) 

808 

809 # combine all files 

810 files.extend(plus_files) 

811 

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

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

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

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

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

817 if winsvc: 

818 config = configparser.ConfigParser() 

819 config.read(files) 

820 section = f'{appname}.config' 

821 if config.has_section(section): 

822 option = f'winsvc.{winsvc}' 

823 if config.has_option(section, option): 

824 # replace file paths with whatever config value says 

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

826 

827 return files 

828 

829 

830def make_config( 

831 files=None, 

832 plus_files=None, 

833 appname='wutta', 

834 env_files_name=None, 

835 env_plus_files_name=None, 

836 env=None, 

837 default_files=None, 

838 winsvc=None, 

839 usedb=None, 

840 preferdb=None, 

841 factory=None, 

842 extend=True, 

843 extension_entry_points=None, 

844 **kwargs): 

845 """ 

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

847 initialized per the given parameters and (usually) further 

848 modified by all registered config extensions. 

849 

850 This function really does 3 things: 

851 

852 * determine the set of config files to use 

853 * pass those files to config factory 

854 * apply extensions to the resulting config object 

855 

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

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

858 

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

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

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

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

863 

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

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

866 

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

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

869 

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

871 Default factory is :class:`WuttaConfig`. 

872 

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

874 registered extensions. 

875 

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

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

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

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

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

881 

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

883 useful for tests.) 

884 

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

886 points section, used to identify registered config extensions. 

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

888 ``appname``. 

889 

890 :returns: The new config object. 

891 """ 

892 # collect file paths 

893 files = get_config_paths( 

894 files=files, 

895 plus_files=plus_files, 

896 appname=appname, 

897 env_files_name=env_files_name, 

898 env_plus_files_name=env_plus_files_name, 

899 env=env, 

900 default_files=default_files, 

901 winsvc=winsvc) 

902 

903 # make config object 

904 if not factory: 

905 factory = WuttaConfig 

906 config = factory(files, appname=appname, 

907 usedb=usedb, preferdb=preferdb, 

908 **kwargs) 

909 

910 # maybe extend config object 

911 if extend: 

912 if not extension_entry_points: 

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

914 

915 # apply all registered extensions 

916 # TODO: maybe let config disable some extensions? 

917 extensions = load_entry_points(extension_entry_points) 

918 for extension in extensions.values(): 

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

920 extension().configure(config) 

921 

922 return config