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
« 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"""
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:: files_read
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.
136 .. attribute:: usedb
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:
142 .. code-block:: ini
144 [wutta.config]
145 usedb = true
147 See also :ref:`where-config-settings-come-from`.
149 .. attribute:: preferdb
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``.
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.
160 .. code-block:: ini
162 [wutta.config]
163 usedb = true
164 preferdb = true
166 See also :ref:`where-config-settings-come-from`.
167 """
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 = []
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)
187 # add config for use w/ setdefault()
188 self.defaults = configuration.Configuration(defaults)
189 configs.append(self.defaults)
191 # master config set
192 self.configuration = configuration.ConfigurationSet(*configs)
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()
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)
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)
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)
228 log.debug("config files read: %s", self.files_read)
230 def _load_ini_configs(self, path, configs, require=True):
231 path = os.path.abspath(path)
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
242 # ok add that one to the mix
243 configs.append(config)
244 self.files_read.append(path)
246 # need parent folder of that path, for %(here)s interpolation
247 here = os.path.dirname(path)
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)
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)
263 def get_prioritized_files(self):
264 """
265 Returns list of config files in order of priority.
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
272 def setdefault(
273 self,
274 key,
275 value):
276 """
277 Establish a default config value for the given key.
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.
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)
291 # get current value, sans db
292 return self.get(key, usedb=False)
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.
308 .. warning::
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:
315 .. code-block:: ini
317 [foo]
318 bar = 1
319 bar.baz = 2
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::
325 {'baz': '2'}
327 And similarly, if you invoke ``config.get('foo')`` then
328 the return value "should be" the dict::
330 {'bar': '1',
331 'bar.baz': '2'}
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).
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.
348 :param key: String key for which value should be returned.
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.
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``).
358 Note that it is an error to specify a default value if you
359 also specify ``require=True``.
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.
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.
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.
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.
381 :param session: Optional SQLAlchemy session to use for DB lookups.
382 NOTE: This param is not yet implemented; currently ignored.
384 :returns: Value as string.
386 """
387 if require and default is not UNSPECIFIED:
388 raise ValueError("must not specify default value when require=True")
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
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
402 # read from defaults + INI files
403 value = self.configuration.get(key)
404 if value is not None:
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
413 if not ignore_ambiguous:
414 log.warning("ambiguous config key '%s' returns: %s", key, value)
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
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}")
427 # give the default value if specified
428 if default is not UNSPECIFIED:
429 return default
431 def get_from_db(self, key, session=None):
432 """
433 Retrieve a config value from database settings table.
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)
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::
447 config.get('foo', require=True)
449 config.require('foo')
450 """
451 kwargs['require'] = True
452 return self.get(*args, **kwargs)
454 def get_bool(self, *args, **kwargs):
455 """
456 Retrieve a boolean value from config.
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)
465 def get_int(self, *args, **kwargs):
466 """
467 Retrieve an integer value from config.
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)
477 def get_list(self, *args, **kwargs):
478 """
479 Retrieve a list value from config.
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()`.
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)
492 def get_dict(self, prefix):
493 """
494 Retrieve a particular group of values, as a dictionary.
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``.
500 For example given this config file:
502 .. code-block:: ini
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
510 One can get the "dict" for SQLAlchemy engine config via::
512 config.get_dict('wutta.db')
514 And the dict would look like::
516 {'keys': 'default, host',
517 'default.url': 'sqlite:///tmp/default.sqlite',
518 'host.url': 'sqlite:///tmp/host.sqlite',
519 'host.pool_pre_ping': 'true'}
521 :param prefix: String prefix corresponding to a subsection of
522 the config.
524 :returns: Dictionary containing the config subsection.
525 """
526 try:
527 values = self.configuration[prefix]
528 except KeyError:
529 return {}
531 return values.as_dict()
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)
550 def _write_logging_config_file(self):
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)
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
566 def get_app(self):
567 """
568 Returns the global :class:`~wuttjamaican.app.AppHandler`
569 instance, creating it if necessary.
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
581class WuttaConfigExtension:
582 """
583 Base class for all config extensions.
584 """
585 key = None
587 def __repr__(self):
588 return f"WuttaConfigExtension(key={self.key})"
590 def configure(self, config):
591 """
592 Subclass should override this method, to extend the config
593 object in any way necessary.
594 """
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.
603 :param appname: App name to be used as basis for default filenames.
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 []
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'),
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 ]
628 # default paths for *nix
629 return [
630 f'{sys.prefix}/app/{appname}.conf',
632 os.path.expanduser(f'~/.{appname}/{appname}.conf'),
633 os.path.expanduser(f'~/.{appname}.conf'),
635 f'/usr/local/etc/{appname}/{appname}.conf',
636 f'/usr/local/etc/{appname}.conf',
638 f'/etc/{appname}/{appname}.conf',
639 f'/etc/{appname}.conf',
640 ]
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()`.
657 In short, the files to be used are determined by typical priority:
659 * function params - ``files`` and ``plus_files``
660 * environment variables - e.g. ``WUTTA_CONFIG_FILES``
661 * app defaults - e.g. :func:`generic_default_files()`
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.
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.
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.
676 :param plus_files: Explicit set of "plus" config files. Same
677 rules apply here as for the ``files`` param.
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``.
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``.
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``.
692 :param env: Optional environment dict; if not specified
693 ``os.environ`` is used.
695 :param default_files: Optional lookup for "default" file paths.
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.
701 If not specified, :func:`generic_default_files()` will be used
702 for the lookup.
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::
708 mydefaults = '/tmp/something.conf'
710 mydefaults = [
711 '/tmp/something.conf',
712 '/tmp/else.conf',
713 ]
715 def mydefaults(appname):
716 return [
717 f"/tmp/{appname}.conf",
718 f"/tmp/{appname}.ini",
719 ]
721 files = get_config_paths(default_files=mydefaults)
723 :param winsvc: Optional internal name of the Windows service for
724 which the config object is being made.
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.
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.
736 So for example on Windows you might have a config file at
737 ``C:\\ProgramData\\rattail\\rattail.conf`` with contents:
739 .. code-block:: ini
741 [rattail.config]
742 winsvc.RattailFileMonitor = C:\\ProgramData\\rattail\\filemon.conf
744 And then ``C:\\ProgramData\\rattail\\filemon.conf`` would have
745 the actual config for the filemon service.
747 When the service starts it calls::
749 make_config(winsvc='RattailFileMonitor')
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::
757 make_config(files=[r'C:\\ProgramData\\rattail\\filemon.conf'])
759 :returns: List of file paths.
760 """
761 if env is None:
762 env = os.environ
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'
769 files = env.get(env_files_name)
770 if files is not None:
771 files = files.split(os.pathsep)
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)
781 else:
782 files = []
783 for path in generic_default_files(appname):
784 if os.path.exists(path):
785 files.append(path)
787 elif isinstance(files, str):
788 files = [files]
789 else:
790 files = list(files)
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'
797 plus_files = env.get(env_plus_files_name)
798 if plus_files is not None:
799 plus_files = plus_files.split(os.pathsep)
801 else:
802 plus_files = []
804 elif isinstance(plus_files, str):
805 plus_files = [plus_files]
806 else:
807 plus_files = list(plus_files)
809 # combine all files
810 files.extend(plus_files)
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))
827 return files
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.
850 This function really does 3 things:
852 * determine the set of config files to use
853 * pass those files to config factory
854 * apply extensions to the resulting config object
856 Some params are described in :func:`get_config_paths()` since they
857 are passed as-is to that function for the first step.
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`.
864 :param usedb: Passed to the config factory; becomes
865 :attr:`~wuttjamaican.conf.WuttaConfig.usedb`.
867 :param preferdb: Passed to the config factory; becomes
868 :attr:`~wuttjamaican.conf.WuttaConfig.preferdb`.
870 :param factory: Optional factory to use when making the object.
871 Default factory is :class:`WuttaConfig`.
873 :param extend: Whether to "auto-extend" the config with all
874 registered extensions.
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.
882 (Why anyone would need this, is another question..maybe only
883 useful for tests.)
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``.
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)
903 # make config object
904 if not factory:
905 factory = WuttaConfig
906 config = factory(files, appname=appname,
907 usedb=usedb, preferdb=preferdb,
908 **kwargs)
910 # maybe extend config object
911 if extend:
912 if not extension_entry_points:
913 extension_entry_points = f'{appname}.config.extensions'
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)
922 return config