Coverage for adhoc-cicd-odoo-odoo / odoo / tools / config.py: 62%
594 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:22 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:22 +0000
1# Part of Odoo. See LICENSE file for full copyright and licensing details.
3import collections
4import configparser as ConfigParser
5import errno
6import functools
7import logging
8import optparse
9import glob
10import os
11import sys
12import tempfile
13import warnings
14from os.path import expandvars, expanduser, abspath, realpath, normcase
15from odoo import release
16from odoo.tools.func import classproperty
17from . import appdirs
19from passlib.context import CryptContext
21crypt_context = CryptContext(schemes=['pbkdf2_sha512', 'plaintext'],
22 deprecated=['plaintext'],
23 pbkdf2_sha512__rounds=600_000)
25_dangerous_logger = logging.getLogger(__name__) # use config._log() instead
27optparse._ = str # disable gettext
29ALL_DEV_MODE = ['access', 'qweb', 'reload', 'xml']
30DEFAULT_SERVER_WIDE_MODULES = ['base', 'rpc', 'web']
31REQUIRED_SERVER_WIDE_MODULES = ['base', 'web']
34class _Empty:
35 def __repr__(self):
36 return ''
37EMPTY = _Empty()
40class _OdooOption(optparse.Option):
41 config = None # must be overriden
43 TYPES = ['int', 'float', 'string', 'choice', 'bool', 'path', 'comma',
44 'addons_path', 'upgrade_path', 'pre_upgrade_scripts', 'without_demo']
46 @classproperty
47 def TYPE_CHECKER(cls):
48 return {
49 'int': lambda _option, _opt, value: int(value),
50 'float': lambda _option, _opt, value: float(value),
51 'string': lambda _option, _opt, value: str(value),
52 'choice': optparse.check_choice,
53 'bool': cls.config._check_bool,
54 'path': cls.config._check_path,
55 'comma': cls.config._check_comma,
56 'addons_path': cls.config._check_addons_path,
57 'upgrade_path': cls.config._check_upgrade_path,
58 'pre_upgrade_scripts': cls.config._check_scripts,
59 'without_demo': cls.config._check_without_demo,
60 }
62 @classproperty
63 def TYPE_FORMATTER(cls):
64 return {
65 'int': cls.config._format_string,
66 'float': cls.config._format_string,
67 'string': cls.config._format_string,
68 'choice': cls.config._format_string,
69 'bool': cls.config._format_string,
70 'path': cls.config._format_string,
71 'comma': cls.config._format_list,
72 'addons_path': cls.config._format_list,
73 'upgrade_path': cls.config._format_list,
74 'pre_upgrade_scripts': cls.config._format_list,
75 'without_demo': cls.config._format_without_demo,
76 }
78 def __init__(self, *opts, **attrs):
79 self.my_default = attrs.pop('my_default', None)
80 self.cli_loadable = attrs.pop('cli_loadable', True)
81 env_name = attrs.pop('env_name', None)
82 self.env_name = env_name or ''
83 self.file_loadable = attrs.pop('file_loadable', True)
84 self.file_exportable = attrs.pop('file_exportable', self.file_loadable)
85 self.nargs_ = attrs.get('nargs')
86 if self.nargs_ == '?':
87 const = attrs.pop('const', None)
88 attrs['nargs'] = 1
89 attrs.setdefault('metavar', attrs.get('type', 'string').upper())
90 super().__init__(*opts, **attrs)
91 if 'default' in attrs: 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was never true
92 self.config._log(logging.WARNING, "please use my_default= instead of default= with option %s", self)
93 if self.file_exportable and not self.file_loadable: 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true
94 e = (f"it makes no sense that the option {self} can be exported "
95 "to the config file but not loaded from the config file")
96 raise ValueError(e)
97 is_new_option = False
98 if self.dest and self.dest not in self.config.options_index:
99 self.config.options_index[self.dest] = self
100 is_new_option = True
101 if self.nargs_ == '?':
102 self.const = const
103 for opt in self._short_opts + self._long_opts:
104 self.config.optional_options[opt] = self
105 if env_name is None and is_new_option and self.file_loadable:
106 # generate an env_name for file_loadable settings that are in the index
107 self.env_name = 'ODOO_' + self.dest.upper()
108 elif env_name and not is_new_option: 108 ↛ 109line 108 didn't jump to line 109 because the condition on line 108 was never true
109 raise ValueError(f"cannot set env_name to an option that is not indexed: {self}")
111 def __str__(self):
112 out = []
113 if self.cli_loadable:
114 out.append(super().__str__()) # e.g. -i/--init
115 if self.file_loadable:
116 out.append(self.dest)
117 return '/'.join(out)
120class _FileOnlyOption(_OdooOption):
121 def __init__(self, **attrs):
122 super().__init__(**attrs, cli_loadable=False, help=optparse.SUPPRESS_HELP)
124 def _check_opt_strings(self, opts):
125 if opts: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true
126 raise TypeError("No option can be supplied")
128 def _set_opt_strings(self, opts):
129 return
132class _PosixOnlyOption(_OdooOption):
133 def __init__(self, *opts, **attrs):
134 if os.name != 'posix': 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true
135 attrs['help'] = optparse.SUPPRESS_HELP
136 attrs['cli_loadable'] = False
137 attrs['env_name'] = ''
138 attrs['file_loadable'] = False
139 attrs['file_exportable'] = False
140 super().__init__(*opts, **attrs)
143def _deduplicate_loggers(loggers):
144 """ Avoid saving multiple logging levels for the same loggers to a save
145 file, that just takes space and the list can potentially grow unbounded
146 if for some odd reason people use :option`--save`` all the time.
147 """
148 # dict(iterable) -> the last item of iterable for any given key wins,
149 # which is what we want and expect. Output order should not matter as
150 # there are no duplicates within the output sequence
151 return (
152 '{}:{}'.format(logger, level)
153 for logger, level in dict(it.split(':') for it in loggers).items()
154 )
157class configmanager:
158 def __init__(self):
159 self._default_options = {}
160 self._file_options = {}
161 self._env_options = {}
162 self._cli_options = {}
163 self._runtime_options = {}
164 self.options = collections.ChainMap(
165 self._runtime_options,
166 self._cli_options,
167 self._env_options,
168 self._file_options,
169 self._default_options,
170 )
172 # dictionary mapping option destination (keys in self.options) to OdooOptions.
173 self.options_index = {}
175 # list of nargs='?' options, indexed by short/long option (-x, --xx)
176 self.optional_options = {}
178 # map old name -> new name
179 self.aliases = {
180 "import_image_maxbytes": "import_file_maxbytes",
181 "import_image_regex": "import_url_regex",
182 "import_image_timeout": "import_file_timeout",
183 }
185 self.parser = self._build_cli()
186 self._load_default_options()
187 self._parse_config()
189 @property
190 def rcfile(self):
191 self._warn("Since 19.0, use odoo.tools.config['config'] instead", DeprecationWarning, stacklevel=2)
192 return self['config']
194 @rcfile.setter
195 def rcfile(self, rcfile):
196 self._warn(f"Since 19.0, use odoo.tools.config['config'] = {rcfile!r} instead", DeprecationWarning, stacklevel=2)
197 self._runtime_options['config'] = rcfile
199 def _build_cli(self):
200 OdooOption = type('OdooOption', (_OdooOption,), {'config': self})
201 FileOnlyOption = type('FileOnlyOption', (_FileOnlyOption, OdooOption), {})
202 PosixOnlyOption = type('PosixOnlyOption', (_PosixOnlyOption, OdooOption), {})
204 version = "%s %s" % (release.description, release.version)
205 parser = optparse.OptionParser(version=version, option_class=OdooOption)
207 parser.add_option(FileOnlyOption(dest='admin_passwd', my_default='admin'))
208 parser.add_option(FileOnlyOption(dest='bin_path', type='path', my_default='', file_exportable=False))
209 parser.add_option(FileOnlyOption(dest='csv_internal_sep', my_default=','))
210 parser.add_option(FileOnlyOption(dest='default_productivity_apps', type='bool', my_default=False, file_exportable=False))
211 parser.add_option(FileOnlyOption(dest='import_file_maxbytes', type='int', my_default=10 * 1024 * 1024, file_exportable=False))
212 parser.add_option(FileOnlyOption(dest='import_file_timeout', type='int', my_default=3, file_exportable=False))
213 parser.add_option(FileOnlyOption(dest='import_url_regex', my_default=r"^(?:http|https)://", file_exportable=False))
214 parser.add_option(FileOnlyOption(dest='proxy_access_token', my_default='', file_exportable=False))
215 parser.add_option(FileOnlyOption(dest='publisher_warranty_url', my_default='http://services.odoo.com/publisher-warranty/', file_exportable=False))
216 parser.add_option(FileOnlyOption(dest='reportgz', action='store_true', my_default=False))
217 parser.add_option(FileOnlyOption(dest='websocket_keep_alive_timeout', type='int', my_default=3600))
218 parser.add_option(FileOnlyOption(dest='websocket_rate_limit_burst', type='int', my_default=10))
219 parser.add_option(FileOnlyOption(dest='websocket_rate_limit_delay', type='float', my_default=0.2))
221 # Server startup config
222 group = optparse.OptionGroup(parser, "Common options")
223 group.add_option("-c", "--config", dest="config", type='path', file_loadable=False, env_name='ODOO_RC',
224 help="specify alternate config file")
225 group.add_option("-s", "--save", action="store_true", dest="save", my_default=False, file_loadable=False,
226 help="save configuration to ~/.odoorc (or to ~/.openerp_serverrc if it exists)")
227 group.add_option("-i", "--init", dest="init", type='comma', metavar="MODULE,...", my_default=[], file_loadable=False,
228 help="install one or more modules (comma-separated list, use \"all\" for all modules), requires -d")
229 group.add_option("-u", "--update", dest="update", type='comma', metavar="MODULE,...", my_default=[], file_loadable=False,
230 help="update one or more modules (comma-separated list, use \"all\" for all modules). Requires -d.")
231 group.add_option("--reinit", dest="reinit", type='comma', metavar="MODULE,...", my_default=[], file_loadable=False,
232 help="reinitialize one or more modules (comma-separated list), requires -d")
233 group.add_option("--with-demo", dest="with_demo", action='store_true', my_default=False,
234 help="install demo data in new databases")
235 group.add_option("--without-demo", dest="with_demo", type='without_demo', metavar='BOOL', nargs='?', const=True,
236 help="don't install demo data in new databases (default)")
237 group.add_option("--skip-auto-install", dest="skip_auto_install", action="store_true", my_default=False,
238 help="skip the automatic installation of modules marked as auto_install")
239 group.add_option("-P", "--import-partial", dest="import_partial", type='path', my_default='', file_loadable=False,
240 help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.")
241 group.add_option("--pidfile", dest="pidfile", type='path', my_default='',
242 help="file where the server pid will be stored")
243 group.add_option("--addons-path", dest="addons_path", type='addons_path', metavar='PATH,...', my_default=[],
244 help="specify additional addons paths (separated by commas).")
245 group.add_option("--upgrade-path", dest="upgrade_path", type='upgrade_path', metavar='PATH,...', my_default=[],
246 help="specify an additional upgrade path.")
247 group.add_option('--pre-upgrade-scripts', dest='pre_upgrade_scripts', type='pre_upgrade_scripts', metavar='PATH,...', my_default=[],
248 help="Run specific upgrade scripts before loading any module when -u is provided.")
249 group.add_option("--load", dest="server_wide_modules", type='comma', metavar='MODULE,...', my_default=DEFAULT_SERVER_WIDE_MODULES,
250 help="Comma-separated list of server-wide modules.")
251 group.add_option("-D", "--data-dir", dest="data_dir", type='path', # sensitive default set in _load_default_options
252 help="Directory where to store Odoo data")
253 parser.add_option_group(group)
255 # HTTP
256 group = optparse.OptionGroup(parser, "HTTP Service Configuration")
257 group.add_option("--http-interface", dest="http_interface", my_default='0.0.0.0',
258 help="Listen interface address for HTTP services.")
259 group.add_option("-p", "--http-port", dest="http_port", my_default=8069,
260 help="Listen port for the main HTTP service", type="int", metavar="PORT")
261 group.add_option("--gevent-port", dest="gevent_port", my_default=8072,
262 help="Listen port for the gevent worker", type="int", metavar="PORT")
263 group.add_option("--no-http", dest="http_enable", action="store_false", my_default=True,
264 help="Disable the HTTP and Longpolling services entirely")
265 group.add_option("--proxy-mode", dest="proxy_mode", action="store_true", my_default=False,
266 help="Activate reverse proxy WSGI wrappers (headers rewriting) "
267 "Only enable this when running behind a trusted web proxy!")
268 group.add_option("--x-sendfile", dest="x_sendfile", action="store_true", my_default=False,
269 help="Activate X-Sendfile (apache) and X-Accel-Redirect (nginx) "
270 "HTTP response header to delegate the delivery of large "
271 "files (assets/attachments) to the web server.")
272 parser.add_option_group(group)
274 # WEB
275 group = optparse.OptionGroup(parser, "Web interface Configuration")
276 group.add_option("--db-filter", dest="dbfilter", my_default='', metavar="REGEXP",
277 help="Regular expressions for filtering available databases for Web UI. "
278 "The expression can use %d (domain) and %h (host) placeholders.")
279 parser.add_option_group(group)
281 # Testing Group
282 group = optparse.OptionGroup(parser, "Testing Configuration")
283 group.add_option("--test-file", dest="test_file", type='path', my_default='', file_loadable=False,
284 help="Launch a python test file.")
285 group.add_option("--test-enable", dest='test_enable', action="store_true", file_loadable=False,
286 help="Enable unit tests. Implies --stop-after-init")
287 group.add_option("-t", "--test-tags", dest="test_tags", file_loadable=False,
288 help="Comma-separated list of specs to filter which tests to execute. Enable unit tests if set. "
289 "A filter spec has the format: [-][tag][/module][:class][.method][[params]] "
290 "The '-' specifies if we want to include or exclude tests matching this spec. "
291 "The tag will match tags added on a class with a @tagged decorator "
292 "(all Test classes have 'standard' and 'at_install' tags "
293 "until explicitly removed, see the decorator documentation). "
294 "'*' will match all tags. "
295 "If tag is omitted on include mode, its value is 'standard'. "
296 "If tag is omitted on exclude mode, its value is '*'. "
297 "The module, class, and method will respectively match the module name, test class name and test method name. "
298 "Example: --test-tags :TestClass.test_func,/test_module,external "
299 "It is also possible to provide parameters to a test method that supports them"
300 "Example: --test-tags /web.test_js[mail]"
301 "If negated, a test-tag with parameter will negate the parameter when passing it to the test"
303 "Filtering and executing the tests happens twice: right "
304 "after each module installation/update and at the end "
305 "of the modules loading. At each stage tests are filtered "
306 "by --test-tags specs and additionally by dynamic specs "
307 "'at_install' and 'post_install' correspondingly. Implies --stop-after-init")
309 group.add_option("--screencasts", dest="screencasts", type='path', my_default='',
310 metavar='DIR',
311 help="Screencasts will go in DIR/{db_name}/screencasts.")
312 temp_tests_dir = os.path.join(tempfile.gettempdir(), 'odoo_tests')
313 group.add_option("--screenshots", dest="screenshots", type='path', my_default=temp_tests_dir,
314 metavar='DIR',
315 help="Screenshots will go in DIR/{db_name}/screenshots. Defaults to %s." % temp_tests_dir)
316 parser.add_option_group(group)
318 # Logging Group
319 group = optparse.OptionGroup(parser, "Logging Configuration")
320 group.add_option("--logfile", dest="logfile", type='path', my_default='',
321 help="file where the server log will be stored")
322 group.add_option("--syslog", action="store_true", dest="syslog", my_default=False,
323 help="Send the log to the syslog server")
324 group.add_option('--log-handler', action="append", type='comma', my_default=[':INFO'], metavar="MODULE:LEVEL",
325 help='setup a handler at LEVEL for a given MODULE. An empty MODULE indicates the root logger. '
326 'This option can be repeated. Example: "odoo.orm:DEBUG" or "werkzeug:CRITICAL" (default: ":INFO")')
327 group.add_option('--log-web', action="append_const", dest="log_handler", const=("odoo.http:DEBUG",),
328 help='shortcut for --log-handler=odoo.http:DEBUG')
329 group.add_option('--log-sql', action="append_const", dest="log_handler", const=("odoo.sql_db:DEBUG",),
330 help='shortcut for --log-handler=odoo.sql_db:DEBUG')
331 group.add_option('--log-db', dest='log_db', help="Logging database", my_default='')
332 group.add_option('--log-db-level', dest='log_db_level', my_default='warning', help="Logging database level")
333 # For backward-compatibility, map the old log levels to something
334 # quite close.
335 levels = [
336 'info', 'debug_rpc', 'warn', 'test', 'critical', 'runbot',
337 'debug_sql', 'error', 'debug', 'debug_rpc_answer', 'notset'
338 ]
339 group.add_option('--log-level', dest='log_level', type='choice',
340 choices=levels, my_default='info',
341 help='specify the level of the logging. Accepted values: %s.' % (levels,))
343 parser.add_option_group(group)
345 # SMTP Group
346 group = optparse.OptionGroup(parser, "SMTP Configuration")
347 group.add_option('--email-from', dest='email_from', my_default='',
348 help='specify the SMTP email address for sending email')
349 group.add_option('--from-filter', dest='from_filter', my_default='',
350 help='specify for which email address the SMTP configuration can be used')
351 group.add_option('--smtp', dest='smtp_server', my_default='localhost',
352 help='specify the SMTP server for sending email')
353 group.add_option('--smtp-port', dest='smtp_port', my_default=25,
354 help='specify the SMTP port', type="int")
355 group.add_option('--smtp-ssl', dest='smtp_ssl', action='store_true', my_default=False,
356 help='if passed, SMTP connections will be encrypted with SSL (STARTTLS)')
357 group.add_option('--smtp-user', dest='smtp_user', my_default='',
358 help='specify the SMTP username for sending email')
359 group.add_option('--smtp-password', dest='smtp_password', my_default='',
360 help='specify the SMTP password for sending email')
361 group.add_option('--smtp-ssl-certificate-filename', dest='smtp_ssl_certificate_filename', type='path', my_default='',
362 help='specify the SSL certificate used for authentication')
363 group.add_option('--smtp-ssl-private-key-filename', dest='smtp_ssl_private_key_filename', type='path', my_default='',
364 help='specify the SSL private key used for authentication')
365 parser.add_option_group(group)
367 # Database Group
368 group = optparse.OptionGroup(parser, "Database related options")
369 group.add_option("-d", "--database", dest="db_name", type='comma', metavar="DATABASE,...", my_default=[], env_name='PGDATABASE',
370 help="database(s) used when installing or updating modules.")
371 group.add_option("-r", "--db_user", dest="db_user", my_default='', env_name='PGUSER',
372 help="specify the database user name")
373 group.add_option("-w", "--db_password", dest="db_password", my_default='', env_name='PGPASSWORD',
374 help="specify the database password")
375 group.add_option("--pg_path", dest="pg_path", type='path', my_default='', env_name='PGPATH',
376 help="specify the pg executable path")
377 group.add_option("--db_host", dest="db_host", my_default='', env_name='PGHOST',
378 help="specify the database host")
379 group.add_option("--db_replica_host", dest="db_replica_host", my_default=None, env_name='PGHOST_REPLICA',
380 help="specify the replica host")
381 group.add_option("--db_port", dest="db_port", my_default=None, env_name='PGPORT',
382 help="specify the database port", type="int")
383 group.add_option("--db_replica_port", dest="db_replica_port", my_default=None, env_name='PGPORT_REPLICA',
384 help="specify the replica port", type="int")
385 group.add_option("--db_sslmode", dest="db_sslmode", type="choice", my_default='prefer', env_name='PGSSLMODE',
386 choices=['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full'],
387 help="specify the database ssl connection mode (see PostgreSQL documentation)")
388 group.add_option("--db_app_name", dest="db_app_name", my_default="odoo-{pid}", env_name='PGAPPNAME',
389 help="specify the application name in the database, {pid} is substituted by the process pid")
390 group.add_option("--db_maxconn", dest="db_maxconn", type='int', my_default=64,
391 help="specify the maximum number of physical connections to PostgreSQL")
392 group.add_option("--db_maxconn_gevent", dest="db_maxconn_gevent", type='int', my_default=None,
393 help="specify the maximum number of physical connections to PostgreSQL specifically for the gevent worker")
394 group.add_option("--db-template", dest="db_template", my_default="template0", env_name='PGDATABASE_TEMPLATE',
395 help="specify a custom database template to create a new database")
396 parser.add_option_group(group)
398 # i18n Group
399 group = optparse.OptionGroup(parser, "Internationalisation options",
400 "Use these options to translate Odoo to another language. "
401 "See i18n section of the user manual. Option '-d' is mandatory. "
402 "Option '-l' is mandatory in case of importation"
403 )
404 group.add_option('--load-language', dest="load_language", file_exportable=False,
405 help="specifies the languages for the translations you want to be loaded")
406 group.add_option("--i18n-overwrite", dest="overwrite_existing_translations", action="store_true", my_default=False, file_exportable=False,
407 help="overwrites existing translation terms on updating a module.")
408 parser.add_option_group(group)
410 # Security Group
411 security = optparse.OptionGroup(parser, 'Security-related options')
412 security.add_option('--no-database-list', action="store_false", dest='list_db', my_default=True,
413 help="Disable the ability to obtain or view the list of databases. "
414 "Also disable access to the database manager and selector, "
415 "so be sure to set a proper --database parameter first")
416 parser.add_option_group(security)
418 # Advanced options
419 group = optparse.OptionGroup(parser, "Advanced options")
420 group.add_option('--dev', dest='dev_mode', type='comma', metavar="FEATURE,...", my_default=[], file_exportable=False, env_name='ODOO_DEV',
421 # optparse uses a fixed 55 chars to print the help no matter the
422 # terminal size, abuse that to align the features
423 help="Enable developer features (comma-separated list, use "
424 '"all" for access,reload,qweb,xml). Available features: '
425 "- access: log the traceback of access errors "
426 "- qweb: log the compiled xml with qweb errors "
427 "- reload: restart server on change in the source code "
428 "- replica: simulate a deployment with readonly replica "
429 "- werkzeug: open a html debugger on http request error "
430 "- xml: read views from the source code, and not the db ")
431 group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False, file_exportable=False, file_loadable=False,
432 help="stop the server after its initialization")
433 group.add_option("--osv-memory-count-limit", dest="osv_memory_count_limit", my_default=0,
434 help="Force a limit on the maximum number of records kept in the virtual "
435 "osv_memory tables. By default there is no limit.",
436 type="int")
437 group.add_option("--transient-age-limit", dest="transient_age_limit", my_default=1.0,
438 help="Time limit (decimal value in hours) records created with a "
439 "TransientModel (mostly wizard) are kept in the database. Default to 1 hour.",
440 type="float")
441 group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=2,
442 help="Maximum number of threads processing concurrently cron jobs (default 2).",
443 type="int")
444 group.add_option("--limit-time-worker-cron", dest="limit_time_worker_cron", my_default=0,
445 help="Maximum time a cron thread/worker stays alive before it is restarted. "
446 "Set to 0 to disable. (default: 0)",
447 type="int")
448 group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true",
449 help="Try to enable the unaccent extension when creating new databases.")
450 group.add_option("--geoip-city-db", "--geoip-db", dest="geoip_city_db", type='path', my_default='/usr/share/GeoIP/GeoLite2-City.mmdb',
451 help="Absolute path to the GeoIP City database file.")
452 group.add_option("--geoip-country-db", dest="geoip_country_db", type='path', my_default='/usr/share/GeoIP/GeoLite2-Country.mmdb',
453 help="Absolute path to the GeoIP Country database file.")
454 parser.add_option_group(group)
456 group = optparse.OptionGroup(parser, "Multiprocessing options")
457 # TODO sensible default for the three following limits.
458 group.add_option(PosixOnlyOption(
459 "--workers", dest="workers", my_default=0,
460 help="Specify the number of workers, 0 disable prefork mode.",
461 type="int"))
462 group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=2048 * 1024 * 1024,
463 help="Maximum allowed virtual memory per worker (in bytes), when reached the worker be "
464 "reset after the current request (default 2048MiB).",
465 type="int")
466 group.add_option(PosixOnlyOption(
467 "--limit-memory-soft-gevent", dest="limit_memory_soft_gevent", my_default=None,
468 help="Maximum allowed virtual memory per gevent worker (in bytes), when reached the worker will be "
469 "reset after the current request. Defaults to `--limit-memory-soft`.",
470 type="int"))
471 group.add_option(PosixOnlyOption(
472 "--limit-memory-hard", dest="limit_memory_hard", my_default=2560 * 1024 * 1024,
473 help="Maximum allowed virtual memory per worker (in bytes), when reached, any memory "
474 "allocation will fail (default 2560MiB).",
475 type="int"))
476 group.add_option(PosixOnlyOption(
477 "--limit-memory-hard-gevent", dest="limit_memory_hard_gevent", my_default=None,
478 help="Maximum allowed virtual memory per gevent worker (in bytes), when reached, any memory "
479 "allocation will fail. Defaults to `--limit-memory-hard`.",
480 type="int"))
481 group.add_option(PosixOnlyOption(
482 "--limit-time-cpu", dest="limit_time_cpu", my_default=60,
483 help="Maximum allowed CPU time per request (default 60).",
484 type="int"))
485 group.add_option("--limit-time-real", dest="limit_time_real", my_default=120,
486 help="Maximum allowed Real time per request (default 120).",
487 type="int")
488 group.add_option("--limit-time-real-cron", dest="limit_time_real_cron", my_default=-1,
489 help="Maximum allowed Real time per cron job. (default: --limit-time-real). "
490 "Set to 0 for no limit. ",
491 type="int")
492 group.add_option(PosixOnlyOption(
493 "--limit-request", dest="limit_request", my_default=2**16,
494 help="Maximum number of request to be processed per worker (default 65536).",
495 type="int"))
496 parser.add_option_group(group)
498 return parser
500 def _load_default_options(self):
501 self._default_options.clear()
502 self._default_options.update({
503 option_name: option.my_default
504 for option_name, option in self.options_index.items()
505 })
507 self._default_options['data_dir'] = (
508 appdirs.user_data_dir(release.product_name, release.author)
509 if os.path.isdir(os.path.expanduser('~')) else
510 appdirs.site_data_dir(release.product_name, release.author)
511 if sys.platform in ['win32', 'darwin'] else
512 f'/var/lib/{release.product_name}'
513 )
515 if os.name == 'nt': 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true
516 rcfilepath = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'odoo.conf')
517 elif os.path.isfile(rcfilepath := os.path.expanduser('~/.odoorc')): 517 ↛ 519line 517 didn't jump to line 519 because the condition on line 517 was always true
518 pass
519 elif os.path.isfile(rcfilepath := os.path.expanduser('~/.openerp_serverrc')):
520 self._warn("Since ages ago, the ~/.openerp_serverrc file has been replaced by ~/.odoorc", DeprecationWarning)
521 else:
522 rcfilepath = '~/.odoorc'
523 self._default_options['config'] = self._normalize(rcfilepath)
525 _log_entries = [] # helpers for log() and warn(), accumulate messages
526 _warn_entries = [] # until logging is configured and the entries flushed
528 @classmethod
529 def _log(cls, loglevel, message, *args, **kwargs):
530 # is replaced by logger.log once logging is ready
531 cls._log_entries.append((loglevel, message, args, kwargs))
533 @classmethod
534 def _warn(cls, message, *args, **kwargs):
535 # is replaced by warnings.warn once logging is ready
536 cls._warn_entries.append((message, args, kwargs))
538 @classmethod
539 def _flush_log_and_warn_entries(cls):
540 for loglevel, message, args, kwargs in cls._log_entries: 540 ↛ 541line 540 didn't jump to line 541 because the loop on line 540 never started
541 _dangerous_logger.log(loglevel, message, *args, **kwargs)
542 cls._log_entries.clear()
543 cls._log = _dangerous_logger.log
545 for message, args, kwargs in cls._warn_entries: 545 ↛ 546line 545 didn't jump to line 546 because the loop on line 545 never started
546 warnings.warn(message, *args, **kwargs, stacklevel=1)
547 cls._warn_entries.clear()
548 cls._warn = warnings.warn
550 def parse_config(self, args: list[str] | None = None, *, setup_logging: bool | None = None) -> None:
551 """ Parse the configuration file (if any) and the command-line
552 arguments.
554 This method initializes odoo.tools.config and openerp.conf (the
555 former should be removed in the future) with library-wide
556 configuration values.
558 This method must be called before proper usage of this library can be
559 made.
561 Typical usage of this method:
563 odoo.tools.config.parse_config(sys.argv[1:])
564 """
565 from odoo import modules, netsvc # noqa: PLC0415
566 opt = self._parse_config(args)
567 if setup_logging is not False: 567 ↛ 579line 567 didn't jump to line 579 because the condition on line 567 was always true
568 netsvc.init_logger()
569 # warn after having done setup, so it has a chance to show up
570 # (mostly once this warning is bumped to DeprecationWarning proper)
571 if setup_logging is None: 571 ↛ 572line 571 didn't jump to line 572 because the condition on line 571 was never true
572 warnings.warn(
573 "As of Odoo 18, it's recommended to specify whether"
574 " you want Odoo to setup its own logging (or want to"
575 " handle it yourself)",
576 category=PendingDeprecationWarning,
577 stacklevel=2,
578 )
579 self._warn_deprecated_options()
580 self._flush_log_and_warn_entries()
581 modules.module.initialize_sys_path()
582 return opt
584 def _parse_config(self, args=None):
585 # preprocess the args to add support for nargs='?'
586 for arg_no, arg in enumerate(args or ()):
587 if option := self.optional_options.get(arg): 587 ↛ 588line 587 didn't jump to line 588 because the condition on line 587 was never true
588 if arg_no == len(args) - 1 or args[arg_no + 1].startswith('-'):
589 args[arg_no] += '=' + self.format(option.dest, option.const)
590 self._log(logging.DEBUG, "changed %s for %s", arg, args[arg_no])
592 opt, unknown_args = self.parser.parse_args(args or [])
593 if unknown_args: 593 ↛ 594line 593 didn't jump to line 594 because the condition on line 593 was never true
594 self.parser.error(f"unrecognized parameters: {' '.join(unknown_args)}")
596 if not opt.save and opt.config and not os.access(opt.config, os.R_OK): 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true
597 self.parser.error(f"the config file {opt.config!r} selected with -c/--config doesn't exist or is not readable, use -s/--save if you want to generate it")
599 # Even if they are not exposed on the CLI, cli un-loadable variables still show up in the opt, remove them
600 for option_name in list(vars(opt).keys()):
601 if not self.options_index[option_name].cli_loadable:
602 delattr(opt, option_name) # hence list(...) above
604 self._load_env_options()
605 self._load_cli_options(opt)
606 self._load_file_options(self['config'])
607 self._postprocess_options()
609 if opt.save: 609 ↛ 610line 609 didn't jump to line 610 because the condition on line 609 was never true
610 self.save()
612 return opt
614 def _load_env_options(self):
615 self._env_options.clear()
616 environ = os.environ
617 for option_name, option in self.options_index.items():
618 env_name = option.env_name
619 if env_name and env_name in environ:
620 self._env_options[option_name] = self.parse(option_name, environ[env_name])
621 if environ.get('OPENERP_SERVER'): 621 ↛ 622line 621 didn't jump to line 622 because the condition on line 621 was never true
622 self._warn("Since ages ago, the OPENERP_SERVER environment variable has been replaced by ODOO_RC", DeprecationWarning)
624 def _load_cli_options(self, opt):
625 # odoo.cli.command.main parses the config twice, the second time
626 # without --addons-path but expect the value to be persisted
627 addons_path = self._cli_options.pop('addons_path', None)
628 self._cli_options.clear()
629 if addons_path is not None: 629 ↛ 630line 629 didn't jump to line 630 because the condition on line 629 was never true
630 self._cli_options['addons_path'] = addons_path
632 keys = [
633 option_name for option_name, option
634 in self.options_index.items()
635 if option.cli_loadable
636 if option.action != 'append'
637 ]
639 for arg in keys:
640 if getattr(opt, arg, None) is not None:
641 self._cli_options[arg] = getattr(opt, arg)
643 if opt.log_handler: 643 ↛ 644line 643 didn't jump to line 644 because the condition on line 643 was never true
644 self._cli_options['log_handler'] = [handler for comma in opt.log_handler for handler in comma]
646 def _postprocess_options(self):
647 self._runtime_options.clear()
649 # check for mutualy exclusive / dependant options
650 if self.options['syslog'] and self.options['logfile']: 650 ↛ 651line 650 didn't jump to line 651 because the condition on line 650 was never true
651 self.parser.error("the syslog and logfile options are exclusive")
653 if self.options['overwrite_existing_translations'] and not self['update']: 653 ↛ 654line 653 didn't jump to line 654 because the condition on line 653 was never true
654 self.parser.error("the i18n-overwrite option cannot be used without the update option")
656 if len(self['db_name']) > 1 and (self['init'] or self['update']): 656 ↛ 657line 656 didn't jump to line 657 because the condition on line 656 was never true
657 self.parser.error("Cannot use -i/--init or -u/--update with multiple databases in the -d/--database/db_name")
659 # ensure default server wide modules are present
660 if not self['server_wide_modules']: 660 ↛ 661line 660 didn't jump to line 661 because the condition on line 660 was never true
661 self._runtime_options['server_wide_modules'] = DEFAULT_SERVER_WIDE_MODULES
662 for mod in REQUIRED_SERVER_WIDE_MODULES:
663 if mod not in self['server_wide_modules']: 663 ↛ 664line 663 didn't jump to line 664 because the condition on line 663 was never true
664 self._log(logging.INFO, "adding missing %r to %s", mod, self.options_index['server_wide_modules'])
665 self._runtime_options['server_wide_modules'] = [mod] + self['server_wide_modules']
667 # accumulate all log_handlers
668 self._runtime_options['log_handler'] = list(_deduplicate_loggers([
669 *self._default_options.get('log_handler', []),
670 *self._file_options.get('log_handler', []),
671 *self._env_options.get('log_handler', []),
672 *self._cli_options.get('log_handler', []),
673 ]))
675 self._runtime_options['init'] = dict.fromkeys(self['init'], True) or {}
676 self._runtime_options['update'] = {'base': True} if 'all' in self['update'] else dict.fromkeys(self['update'], True)
678 # TODO saas-22.1: remove support for the empty db_replica_host
679 if self['db_replica_host'] == '': 679 ↛ 680line 679 didn't jump to line 680 because the condition on line 679 was never true
680 self._runtime_options['db_replica_host'] = None
681 if 'replica' not in self['dev_mode']:
682 # Conditional warning so it is possible to have a single
683 # config file (with db_replica_host= dev_mode=replica)
684 # that works in both 18.0 and 19.0.
685 # TODO saas-21.1:
686 # move this warning out of the if, as 18.0 won't be
687 # supported anymore, so people remove db_replica_host=
688 # from their config.
689 self._warn((
690 "Since 19.0, an empty {replica_host} was the 18.0 "
691 "way to open a replica connection on the same "
692 "server as {db_host}, for development/testing "
693 "purpose, the feature now exists as {dev}=replica"
694 ).format(
695 replica_host=self.options_index['db_replica_host'],
696 db_host=self.options_index['db_host'],
697 dev=self.options_index['dev_mode'],
698 ), DeprecationWarning)
699 self._runtime_options['dev_mode'] = self['dev_mode'] + ['replica']
701 if 'all' in self['dev_mode']: 701 ↛ 702line 701 didn't jump to line 702 because the condition on line 701 was never true
702 self._runtime_options['dev_mode'] = self['dev_mode'] + ALL_DEV_MODE
704 if test_file := self['test_file']: 704 ↛ 705line 704 didn't jump to line 705 because the condition on line 704 was never true
705 if not os.path.isfile(test_file):
706 self._log(logging.WARNING, f'test file {test_file!r} cannot be found')
707 elif not test_file.endswith('.py'):
708 self._log(logging.WARNING, f'test file {test_file!r} is not a python file')
709 else:
710 self._log(logging.INFO, 'Transforming --test-file into --test-tags')
711 test_tags = (self['test_tags'] or '').split(',')
712 test_tags.append(os.path.abspath(self['test_file']))
713 self._runtime_options['test_tags'] = ','.join(test_tags)
714 self._runtime_options['test_enable'] = True
715 if self['test_enable'] and not self['test_tags']: 715 ↛ 716line 715 didn't jump to line 716 because the condition on line 715 was never true
716 self._runtime_options['test_tags'] = "+standard"
717 self._runtime_options['test_enable'] = bool(self['test_tags'])
718 if self._runtime_options['test_enable']: 718 ↛ 719line 718 didn't jump to line 719 because the condition on line 718 was never true
719 self._runtime_options['stop_after_init'] = True
720 if not self['db_name']:
721 self._log(logging.WARNING,
722 "Empty %s, tests won't run", self.options_index['db_name'])
724 def _warn_deprecated_options(self):
725 if self['http_enable'] and not self.http_socket_activation: 725 ↛ 735line 725 didn't jump to line 735 because the condition on line 725 was always true
726 for map_ in self.options.maps: 726 ↛ 735line 726 didn't jump to line 735 because the loop on line 726 didn't complete
727 if 'http_interface' in map_:
728 if map_ is self._file_options and map_['http_interface'] == '': # noqa: PLC1901 728 ↛ 729line 728 didn't jump to line 729 because the condition on line 728 was never true
729 del map_['http_interface']
730 elif map_ is self._default_options: 730 ↛ 731line 730 didn't jump to line 731 because the condition on line 730 was never true
731 self._log(logging.WARNING, "missing %s, using 0.0.0.0 by default, will change to 127.0.0.1 in 20.0", self.options_index['http_interface'])
732 else:
733 break
735 for old_option_name, new_option_name in self.aliases.items():
736 for source_name, deprecated_value in self._get_sources(old_option_name).items():
737 if deprecated_value is EMPTY: 737 ↛ 739line 737 didn't jump to line 739 because the condition on line 737 was always true
738 continue
739 default_value = self._default_options[new_option_name]
740 current_value = self[new_option_name]
742 if deprecated_value in (current_value, default_value):
743 # Surely this is from a --save that was run in a
744 # prior version. There is no point in emitting a
745 # warning because: (1) it holds the same value as
746 # the correct option, and (2) it is going to be
747 # automatically removed on the next --save anyway.
748 self._log(logging.INFO,
749 f"The {old_option_name!r} option found in the "
750 f"{source_name} is a deprecated alias to "
751 f"{new_option_name!r}. The configuration value "
752 "is the same as the default value, it can "
753 "safely be removed.")
754 elif current_value == default_value:
755 # deprecated_value != current_value == default_value
756 # assume the new option was not set
757 self._runtime_options[new_option_name] = self.parse(new_option_name, deprecated_value)
758 self._warn(
759 f"The {old_option_name!r} option found in the "
760 f"{source_name} is a deprecated alias to "
761 f"{new_option_name!r}, please use the latter.",
762 DeprecationWarning)
763 else:
764 # deprecated_value != current_value != default_value
765 self.parser.error(
766 f"The two options {old_option_name!r} "
767 f"(found in the {source_name} but deprecated) "
768 f"and {new_option_name!r} are set to different "
769 "values. Please remove the first one and make "
770 "sure the second is correct."
771 )
773 @classmethod
774 def _is_addons_path(cls, path):
775 for f in os.listdir(path): 775 ↛ 782line 775 didn't jump to line 782 because the loop on line 775 didn't complete
776 modpath = os.path.join(path, f)
778 def hasfile(filename):
779 return os.path.isfile(os.path.join(modpath, filename))
780 if hasfile('__init__.py') and hasfile('__manifest__.py'):
781 return True
782 return False
784 @classmethod
785 def _check_addons_path(cls, option, opt, value):
786 ad_paths = []
787 for path in map(cls._normalize, cls._check_comma(option, opt, value)):
788 if not os.path.isdir(path): 788 ↛ 789line 788 didn't jump to line 789 because the condition on line 788 was never true
789 cls._log(logging.WARNING, "option %s, no such directory %r, skipped", opt, path)
790 continue
791 if not cls._is_addons_path(path): 791 ↛ 792line 791 didn't jump to line 792 because the condition on line 791 was never true
792 cls._log(logging.WARNING, "option %s, invalid addons directory %r, skipped", opt, path)
793 continue
794 ad_paths.append(path)
796 return ad_paths
798 @classmethod
799 def _check_upgrade_path(cls, option, opt, value):
800 upgrade_path = []
801 for path in map(cls._normalize, cls._check_comma(option, opt, value)):
802 if not os.path.isdir(path):
803 cls._log(logging.WARNING, "option %s, no such directory %r, skipped", opt, path)
804 continue
805 if not cls._is_upgrades_path(path):
806 cls._log(logging.WARNING, "option %s, invalid upgrade directory %r, skipped", opt, path)
807 continue
808 if path not in upgrade_path:
809 upgrade_path.append(path)
810 return upgrade_path
812 @classmethod
813 def _check_scripts(cls, option, opt, value):
814 pre_upgrade_scripts = []
815 for path in map(cls._normalize, cls._check_comma(option, opt, value)):
816 if not os.path.isfile(path):
817 cls._log(logging.WARNING, "option %s, no such file %r, skipped", opt, path)
818 continue
819 if path not in pre_upgrade_scripts:
820 pre_upgrade_scripts.append(path)
821 return pre_upgrade_scripts
823 @classmethod
824 def _is_upgrades_path(cls, path):
825 module = '*'
826 version = '*'
827 return any(
828 glob.glob(os.path.join(path, f'{module}/{version}/{prefix}-*.py'))
829 for prefix in ['pre', 'post', 'end']
830 )
832 @classmethod
833 def _check_bool(cls, option, opt, value):
834 if value.lower() in ('1', 'yes', 'true', 'on'):
835 return True
836 if value.lower() in ('0', 'no', 'false', 'off'):
837 return False
838 raise optparse.OptionValueError(
839 f"option {opt}: invalid boolean value: {value!r}"
840 )
842 @classmethod
843 def _check_comma(cls, option_name, option, value):
844 return [v for s in value.split(',') if (v := s.strip())]
846 @classmethod
847 def _check_path(cls, option, opt, value):
848 return cls._normalize(value)
850 @classmethod
851 def _check_without_demo(cls, option, opt, value):
852 # invert the result because it is stored in "with_demo"
853 try:
854 return not cls._check_bool(option, opt, value)
855 except optparse.OptionValueError:
856 cls._log(logging.WARNING, "option %s: since 19.0, invalid boolean value: %r, assume %s", opt, value, value != 'None')
857 return value == 'None'
859 def parse(self, option_name, value):
860 if not isinstance(value, str): 860 ↛ 861line 860 didn't jump to line 861 because the condition on line 860 was never true
861 e = f"can only cast strings: {value!r}"
862 raise TypeError(e)
863 if value == 'None': 863 ↛ 864line 863 didn't jump to line 864 because the condition on line 863 was never true
864 return None
865 option = self.options_index[option_name]
866 if option.action in ('store_true', 'store_false'): 866 ↛ 867line 866 didn't jump to line 867 because the condition on line 866 was never true
867 check_func = self._check_bool
868 else:
869 check_func = self.parser.option_class.TYPE_CHECKER[option.type]
870 return check_func(option, option_name, value)
872 @classmethod
873 def _format_string(cls, value):
874 return str(value)
876 @classmethod
877 def _format_list(cls, value):
878 return ','.join(filter(bool, (str(elem).strip() for elem in value)))
880 @classmethod
881 def _format_without_demo(cls, value):
882 return str(bool(value))
884 def format(self, option_name, value):
885 option = self.options_index[option_name]
886 if option.action in ('store_true', 'store_false'):
887 format_func = self.parser.option_class.TYPE_FORMATTER['bool']
888 else:
889 format_func = self.parser.option_class.TYPE_FORMATTER[option.type]
890 return format_func(value)
892 def load(self):
893 self._warn("Since 19.0, use config._load_file_options instead", DeprecationWarning, stacklevel=2)
894 self._load_file_options(self['config'])
896 def _load_file_options(self, rcfile):
897 self._file_options.clear()
898 p = ConfigParser.RawConfigParser()
899 try:
900 p.read([rcfile])
901 for (name, value) in p.items('options'):
902 if name == 'without_demo': 902 ↛ 903line 902 didn't jump to line 903 because the condition on line 902 was never true
903 name = 'with_demo'
904 value = str(self._check_without_demo(None, 'without_demo', value))
905 option = self.options_index.get(name)
906 if not option: 906 ↛ 907line 906 didn't jump to line 907 because the condition on line 906 was never true
907 if name not in self.aliases:
908 self._log(logging.WARNING,
909 "unknown option %r in the config file at "
910 "%s, option stored as-is, without parsing",
911 name, self['config'],
912 )
913 self._file_options[name] = value
914 continue
915 if not option.file_loadable: 915 ↛ 916line 915 didn't jump to line 916 because the condition on line 915 was never true
916 continue
917 if ( 917 ↛ 923line 917 didn't jump to line 923 because the condition on line 917 was never true
918 value in ('False', 'false')
919 and option.action not in ('store_true', 'store_false', 'callback')
920 and option.nargs_ != '?'
921 ):
922 # "False" used to be the my_default of many non-bool options
923 self._log(logging.WARNING, "option %s reads %r in the config file at %s but isn't a boolean option, skip", name, value, self['config'])
924 continue
925 self._file_options[name] = self.parse(name, value)
926 except IOError:
927 pass
928 except ConfigParser.NoSectionError:
929 pass
931 def save(self, keys=None):
932 p = ConfigParser.RawConfigParser()
933 rc_exists = os.path.exists(self['config'])
934 if rc_exists and keys:
935 p.read([self['config']])
936 if not p.has_section('options'):
937 p.add_section('options')
938 for opt in sorted(self.options):
939 option = self.options_index.get(opt)
940 if keys is not None and opt not in keys:
941 continue
942 if opt == 'version' or (option and not option.file_exportable):
943 continue
944 if option:
945 p.set('options', opt, self.format(opt, self.options[opt]))
946 else:
947 p.set('options', opt, self.options[opt])
949 # try to create the directories and write the file
950 try:
951 if not rc_exists and not os.path.exists(os.path.dirname(self['config'])):
952 os.makedirs(os.path.dirname(self['config']))
953 try:
954 with open(self['config'], 'w', encoding='utf-8') as file:
955 p.write(file)
956 if not rc_exists:
957 os.chmod(self['config'], 0o600)
958 except IOError:
959 sys.stderr.write("ERROR: couldn't write the config file\n")
961 except OSError:
962 # what to do if impossible?
963 sys.stderr.write("ERROR: couldn't create the config directory\n")
965 def get(self, key, default=None):
966 return self.options.get(key, default)
968 def __setitem__(self, key, value):
969 if isinstance(value, str) and key in self.options_index:
970 value = self.parse(key, value)
971 self.options[key] = value
973 def __getitem__(self, key):
974 return self.options[key]
976 @functools.cached_property
977 def root_path(self):
978 return self._normalize(os.path.join(os.path.dirname(__file__), '..'))
980 @property
981 def addons_base_dir(self):
982 return os.path.join(self.root_path, 'addons')
984 @property
985 def addons_community_dir(self):
986 return os.path.join(os.path.dirname(self.root_path), 'addons')
988 @property
989 def addons_data_dir(self):
990 add_dir = os.path.join(self['data_dir'], 'addons')
991 d = os.path.join(add_dir, release.series)
992 if not os.path.exists(d):
993 try:
994 # bootstrap parent dir +rwx
995 if not os.path.exists(add_dir): 995 ↛ 998line 995 didn't jump to line 998 because the condition on line 995 was always true
996 os.makedirs(add_dir, 0o700)
997 # try to make +rx placeholder dir, will need manual +w to activate it
998 os.makedirs(d, 0o500)
999 except OSError:
1000 self._log(logging.DEBUG, 'Failed to create addons data dir %s', d)
1001 return d
1003 @property
1004 def session_dir(self):
1005 d = os.path.join(self['data_dir'], 'sessions')
1006 try:
1007 os.makedirs(d, 0o700)
1008 except OSError as e:
1009 if e.errno != errno.EEXIST:
1010 raise
1011 assert os.access(d, os.W_OK), \
1012 "%s: directory is not writable" % d
1013 return d
1015 def filestore(self, dbname):
1016 return os.path.join(self['data_dir'], 'filestore', dbname)
1018 def set_admin_password(self, new_password):
1019 self.options['admin_passwd'] = crypt_context.hash(new_password)
1021 def verify_admin_password(self, password):
1022 """Verifies the super-admin password, possibly updating the stored hash if needed"""
1023 stored_hash = self.options['admin_passwd']
1024 if not stored_hash:
1025 # empty password/hash => authentication forbidden
1026 return False
1027 result, updated_hash = crypt_context.verify_and_update(password, stored_hash)
1028 if result:
1029 if updated_hash:
1030 self.options['admin_passwd'] = updated_hash
1031 return True
1032 return False
1034 @property
1035 def http_socket_activation(self):
1036 return (
1037 self['http_enable']
1038 and os.getenv('LISTEN_FDS') == '1'
1039 and os.getenv('LISTEN_PID') == str(os.getpid())
1040 )
1042 @classmethod
1043 def _normalize(cls, path):
1044 if not path: 1044 ↛ 1045line 1044 didn't jump to line 1045 because the condition on line 1044 was never true
1045 return ''
1046 return normcase(realpath(abspath(expanduser(expandvars(path.strip())))))
1048 def _get_sources(self, name):
1049 """Extract the option from the many sources"""
1050 return {
1051 **{
1052 f'source#{no}': source.get(name, EMPTY)
1053 for no, source in enumerate(self.options.maps[:-4])
1054 },
1055 'runtime': self._runtime_options.get(name, EMPTY),
1056 'command line': self._cli_options.get(name, EMPTY),
1057 'environment variable': self._env_options.get(name, EMPTY),
1058 'configuration file': self._file_options.get(name, EMPTY),
1059 'hardcoded default': self._default_options.get(name, EMPTY),
1060 }
1063config = configmanager()