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
« 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"""
27import configparser
28import importlib
29import logging
30import logging.config
31import os
32import sys
33import tempfile
35import config as configuration
37from wuttjamaican.util import (load_entry_points, load_object,
38 parse_bool, parse_list,
39 UNSPECIFIED)
40from wuttjamaican.exc import ConfigurationError
43log = logging.getLogger(__name__)
46class WuttaConfig:
47 """
48 Configuration class for Wutta Framework
50 A single instance of this class is typically created on app
51 startup, by calling :func:`make_config()`.
53 The global config object is mainly responsible for providing
54 config values to the app, via :meth:`get()` and similar methods.
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:
60 * settings table in the DB
61 * one or more INI files
62 * "defaults" provided by app logic
64 :param files: List of file paths from which to read config values.
66 :param defaults: Initial values to use as defaults. This gets
67 converted to :attr:`defaults` during construction.
69 :param appname: Value to assign for :attr:`appname`.
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()`.
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()`.
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.
83 Attributes available on the config instance:
85 .. attribute:: appname
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.
91 For instance the default ``appname`` value is ``'wutta'`` which
92 means a sample config file might look like:
94 .. code-block:: ini
96 [wutta]
97 app.handler = wuttjamaican.app:AppHandler
99 [wutta.db]
100 default.url = sqlite://
102 But if the ``appname`` value is e.g. ``'rattail'`` then the
103 sample config should instead look like:
105 .. code-block:: ini
107 [rattail]
108 app.handler = wuttjamaican.app:AppHandler
110 [rattail.db]
111 default.url = sqlite://
113 .. attribute:: configuration
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.
121 .. attribute:: defaults
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()`.
129 .. attribute:: default_app_handler_spec
131 Spec string for the default app handler, if config does not
132 specify to use another.
134 The true default for this is ``'wuttjamaican.app:AppHandler'``
135 (aka. :class:`~wuttjamaican.app.AppHandler`).
137 .. attribute:: default_engine_maker_spec
139 Spec string for the default engine maker function, if config
140 does not specify to use another.
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()`).
146 .. attribute:: files_read
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.
153 .. attribute:: usedb
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:
159 .. code-block:: ini
161 [wutta.config]
162 usedb = true
164 See also :ref:`where-config-settings-come-from`.
166 .. attribute:: preferdb
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``.
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.
177 .. code-block:: ini
179 [wutta.config]
180 usedb = true
181 preferdb = true
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'
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 = []
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)
206 # add config for use w/ setdefault()
207 self.defaults = configuration.Configuration(defaults)
208 configs.append(self.defaults)
210 # master config set
211 self.configuration = configuration.ConfigurationSet(*configs)
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()
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)
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)
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)
247 log.debug("config files read: %s", self.files_read)
249 def _load_ini_configs(self, path, configs, require=True):
250 path = os.path.abspath(path)
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
261 # ok add that one to the mix
262 configs.append(config)
263 self.files_read.append(path)
265 # need parent folder of that path, for %(here)s interpolation
266 here = os.path.dirname(path)
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)
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)
282 def get_prioritized_files(self):
283 """
284 Returns list of config files in order of priority.
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
291 def setdefault(
292 self,
293 key,
294 value):
295 """
296 Establish a default config value for the given key.
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.
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)
310 # get current value, sans db
311 return self.get(key, usedb=False)
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.
327 .. warning::
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:
334 .. code-block:: ini
336 [foo]
337 bar = 1
338 bar.baz = 2
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::
344 {'baz': '2'}
346 And similarly, if you invoke ``config.get('foo')`` then
347 the return value "should be" the dict::
349 {'bar': '1',
350 'bar.baz': '2'}
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).
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.
367 :param key: String key for which value should be returned.
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.
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``).
377 Note that it is an error to specify a default value if you
378 also specify ``require=True``.
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.
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.
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.
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.
400 :param session: Optional SQLAlchemy session to use for DB lookups.
401 NOTE: This param is not yet implemented; currently ignored.
403 :returns: Value as string.
405 """
406 if require and default is not UNSPECIFIED:
407 raise ValueError("must not specify default value when require=True")
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
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
421 # read from defaults + INI files
422 value = self.configuration.get(key)
423 if value is not None:
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
432 if not ignore_ambiguous:
433 log.warning("ambiguous config key '%s' returns: %s", key, value)
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
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}")
446 # give the default value if specified
447 if default is not UNSPECIFIED:
448 return default
450 def get_from_db(self, key, session=None):
451 """
452 Retrieve a config value from database settings table.
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)
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::
466 config.get('foo', require=True)
468 config.require('foo')
469 """
470 kwargs['require'] = True
471 return self.get(*args, **kwargs)
473 def get_bool(self, *args, **kwargs):
474 """
475 Retrieve a boolean value from config.
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)
484 def get_int(self, *args, **kwargs):
485 """
486 Retrieve an integer value from config.
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)
496 def get_list(self, *args, **kwargs):
497 """
498 Retrieve a list value from config.
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()`.
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)
511 def get_dict(self, prefix):
512 """
513 Retrieve a particular group of values, as a dictionary.
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``.
519 For example given this config file:
521 .. code-block:: ini
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
529 One can get the "dict" for SQLAlchemy engine config via::
531 config.get_dict('wutta.db')
533 And the dict would look like::
535 {'keys': 'default, host',
536 'default.url': 'sqlite:///tmp/default.sqlite',
537 'host.url': 'sqlite:///tmp/host.sqlite',
538 'host.pool_pre_ping': 'true'}
540 :param prefix: String prefix corresponding to a subsection of
541 the config.
543 :returns: Dictionary containing the config subsection.
544 """
545 try:
546 values = self.configuration[prefix]
547 except KeyError:
548 return {}
550 return values.as_dict()
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)
570 def _write_logging_config_file(self):
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)
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
586 def get_app(self):
587 """
588 Returns the global :class:`~wuttjamaican.app.AppHandler`
589 instance, creating it if necessary.
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
600 def get_engine_maker(self):
601 """
602 Returns a callable to be used for constructing SQLAlchemy
603 engines fromc config.
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)
611 def production(self):
612 """
613 Returns boolean indicating whether the app is running in
614 production mode.
616 This value may be set e.g. in config file:
618 .. code-block:: ini
620 [wutta]
621 production = true
622 """
623 return self.get_bool(f'{self.appname}.production', default=False)
626class WuttaConfigExtension:
627 """
628 Base class for all :term:`config extensions <config extension>`.
629 """
630 key = None
632 def __repr__(self):
633 return f"WuttaConfigExtension(key={self.key})"
635 def configure(self, config):
636 """
637 Subclass should override this method, to extend the config
638 object in any way necessary.
639 """
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.
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 """
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.
659 :param appname: App name to be used as basis for default filenames.
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 []
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'),
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 ]
684 # default paths for *nix
685 return [
686 f'{sys.prefix}/app/{appname}.conf',
688 os.path.expanduser(f'~/.{appname}/{appname}.conf'),
689 os.path.expanduser(f'~/.{appname}.conf'),
691 f'/usr/local/etc/{appname}/{appname}.conf',
692 f'/usr/local/etc/{appname}.conf',
694 f'/etc/{appname}/{appname}.conf',
695 f'/etc/{appname}.conf',
696 ]
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()`.
713 In short, the files to be used are determined by typical priority:
715 * function params - ``files`` and ``plus_files``
716 * environment variables - e.g. ``WUTTA_CONFIG_FILES``
717 * app defaults - e.g. :func:`generic_default_files()`
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.
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.
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.
732 :param plus_files: Explicit set of "plus" config files. Same
733 rules apply here as for the ``files`` param.
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``.
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``.
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``.
748 :param env: Optional environment dict; if not specified
749 ``os.environ`` is used.
751 :param default_files: Optional lookup for "default" file paths.
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.
757 If not specified, :func:`generic_default_files()` will be used
758 for the lookup.
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::
764 mydefaults = '/tmp/something.conf'
766 mydefaults = [
767 '/tmp/something.conf',
768 '/tmp/else.conf',
769 ]
771 def mydefaults(appname):
772 return [
773 f"/tmp/{appname}.conf",
774 f"/tmp/{appname}.ini",
775 ]
777 files = get_config_paths(default_files=mydefaults)
779 :param winsvc: Optional internal name of the Windows service for
780 which the config object is being made.
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.
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.
792 So for example on Windows you might have a config file at
793 ``C:\\ProgramData\\rattail\\rattail.conf`` with contents:
795 .. code-block:: ini
797 [rattail.config]
798 winsvc.RattailFileMonitor = C:\\ProgramData\\rattail\\filemon.conf
800 And then ``C:\\ProgramData\\rattail\\filemon.conf`` would have
801 the actual config for the filemon service.
803 When the service starts it calls::
805 make_config(winsvc='RattailFileMonitor')
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::
813 make_config(files=[r'C:\\ProgramData\\rattail\\filemon.conf'])
815 :returns: List of file paths.
816 """
817 if env is None:
818 env = os.environ
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'
825 files = env.get(env_files_name)
826 if files is not None:
827 files = files.split(os.pathsep)
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)]
839 else:
840 files = []
841 for path in generic_default_files(appname):
842 if os.path.exists(path):
843 files.append(path)
845 elif isinstance(files, str):
846 files = [files]
847 else:
848 files = list(files)
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'
855 plus_files = env.get(env_plus_files_name)
856 if plus_files is not None:
857 plus_files = plus_files.split(os.pathsep)
859 else:
860 plus_files = []
862 elif isinstance(plus_files, str):
863 plus_files = [plus_files]
864 else:
865 plus_files = list(plus_files)
867 # combine all files
868 files.extend(plus_files)
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))
885 return files
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.
908 This function really does 3 things:
910 * determine the set of config files to use
911 * pass those files to config factory
912 * apply extensions to the resulting config object
914 Some params are described in :func:`get_config_paths()` since they
915 are passed as-is to that function for the first step.
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`.
922 :param usedb: Passed to the config factory; becomes
923 :attr:`~wuttjamaican.conf.WuttaConfig.usedb`.
925 :param preferdb: Passed to the config factory; becomes
926 :attr:`~wuttjamaican.conf.WuttaConfig.preferdb`.
928 :param factory: Optional factory to use when making the object.
929 Default factory is :class:`WuttaConfig`.
931 :param extend: Whether to "auto-extend" the config with all
932 registered extensions.
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.
940 (Why anyone would need this, is another question..maybe only
941 useful for tests.)
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``.
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)
961 # make config object
962 if not factory:
963 factory = WuttaConfig
964 config = factory(files, appname=appname,
965 usedb=usedb, preferdb=preferdb,
966 **kwargs)
968 # maybe extend config object
969 if extend:
970 if not extension_entry_points:
971 extension_entry_points = f'{appname}.config.extensions'
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)
981 # let extensions run startup hooks if needed
982 for extension in extensions:
983 extension.startup(config)
985 return config