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

1# Part of Odoo. See LICENSE file for full copyright and licensing details. 

2 

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 

18 

19from passlib.context import CryptContext 

20 

21crypt_context = CryptContext(schemes=['pbkdf2_sha512', 'plaintext'], 

22 deprecated=['plaintext'], 

23 pbkdf2_sha512__rounds=600_000) 

24 

25_dangerous_logger = logging.getLogger(__name__) # use config._log() instead 

26 

27optparse._ = str # disable gettext 

28 

29ALL_DEV_MODE = ['access', 'qweb', 'reload', 'xml'] 

30DEFAULT_SERVER_WIDE_MODULES = ['base', 'rpc', 'web'] 

31REQUIRED_SERVER_WIDE_MODULES = ['base', 'web'] 

32 

33 

34class _Empty: 

35 def __repr__(self): 

36 return '' 

37EMPTY = _Empty() 

38 

39 

40class _OdooOption(optparse.Option): 

41 config = None # must be overriden 

42 

43 TYPES = ['int', 'float', 'string', 'choice', 'bool', 'path', 'comma', 

44 'addons_path', 'upgrade_path', 'pre_upgrade_scripts', 'without_demo'] 

45 

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 } 

61 

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 } 

77 

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}") 

110 

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) 

118 

119 

120class _FileOnlyOption(_OdooOption): 

121 def __init__(self, **attrs): 

122 super().__init__(**attrs, cli_loadable=False, help=optparse.SUPPRESS_HELP) 

123 

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") 

127 

128 def _set_opt_strings(self, opts): 

129 return 

130 

131 

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) 

141 

142 

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 ) 

155 

156 

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 ) 

171 

172 # dictionary mapping option destination (keys in self.options) to OdooOptions. 

173 self.options_index = {} 

174 

175 # list of nargs='?' options, indexed by short/long option (-x, --xx) 

176 self.optional_options = {} 

177 

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 } 

184 

185 self.parser = self._build_cli() 

186 self._load_default_options() 

187 self._parse_config() 

188 

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'] 

193 

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 

198 

199 def _build_cli(self): 

200 OdooOption = type('OdooOption', (_OdooOption,), {'config': self}) 

201 FileOnlyOption = type('FileOnlyOption', (_FileOnlyOption, OdooOption), {}) 

202 PosixOnlyOption = type('PosixOnlyOption', (_PosixOnlyOption, OdooOption), {}) 

203 

204 version = "%s %s" % (release.description, release.version) 

205 parser = optparse.OptionParser(version=version, option_class=OdooOption) 

206 

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)) 

220 

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) 

254 

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) 

273 

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) 

280 

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" 

302 

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") 

308 

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) 

317 

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,)) 

342 

343 parser.add_option_group(group) 

344 

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) 

366 

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) 

397 

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) 

409 

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) 

417 

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) 

455 

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) 

497 

498 return parser 

499 

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 }) 

506 

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 ) 

514 

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) 

524 

525 _log_entries = [] # helpers for log() and warn(), accumulate messages 

526 _warn_entries = [] # until logging is configured and the entries flushed 

527 

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)) 

532 

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)) 

537 

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 

544 

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 

549 

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. 

553 

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. 

557 

558 This method must be called before proper usage of this library can be 

559 made. 

560 

561 Typical usage of this method: 

562 

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 

583 

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]) 

591 

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)}") 

595 

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") 

598 

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 

603 

604 self._load_env_options() 

605 self._load_cli_options(opt) 

606 self._load_file_options(self['config']) 

607 self._postprocess_options() 

608 

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() 

611 

612 return opt 

613 

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) 

623 

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 

631 

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 ] 

638 

639 for arg in keys: 

640 if getattr(opt, arg, None) is not None: 

641 self._cli_options[arg] = getattr(opt, arg) 

642 

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] 

645 

646 def _postprocess_options(self): 

647 self._runtime_options.clear() 

648 

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") 

652 

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") 

655 

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") 

658 

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'] 

666 

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 ])) 

674 

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) 

677 

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'] 

700 

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 

703 

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']) 

723 

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 

734 

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] 

741 

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 ) 

772 

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) 

777 

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 

783 

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) 

795 

796 return ad_paths 

797 

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 

811 

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 

822 

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 ) 

831 

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 ) 

841 

842 @classmethod 

843 def _check_comma(cls, option_name, option, value): 

844 return [v for s in value.split(',') if (v := s.strip())] 

845 

846 @classmethod 

847 def _check_path(cls, option, opt, value): 

848 return cls._normalize(value) 

849 

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' 

858 

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) 

871 

872 @classmethod 

873 def _format_string(cls, value): 

874 return str(value) 

875 

876 @classmethod 

877 def _format_list(cls, value): 

878 return ','.join(filter(bool, (str(elem).strip() for elem in value))) 

879 

880 @classmethod 

881 def _format_without_demo(cls, value): 

882 return str(bool(value)) 

883 

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) 

891 

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']) 

895 

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 

930 

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]) 

948 

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") 

960 

961 except OSError: 

962 # what to do if impossible? 

963 sys.stderr.write("ERROR: couldn't create the config directory\n") 

964 

965 def get(self, key, default=None): 

966 return self.options.get(key, default) 

967 

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 

972 

973 def __getitem__(self, key): 

974 return self.options[key] 

975 

976 @functools.cached_property 

977 def root_path(self): 

978 return self._normalize(os.path.join(os.path.dirname(__file__), '..')) 

979 

980 @property 

981 def addons_base_dir(self): 

982 return os.path.join(self.root_path, 'addons') 

983 

984 @property 

985 def addons_community_dir(self): 

986 return os.path.join(os.path.dirname(self.root_path), 'addons') 

987 

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 

1002 

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 

1014 

1015 def filestore(self, dbname): 

1016 return os.path.join(self['data_dir'], 'filestore', dbname) 

1017 

1018 def set_admin_password(self, new_password): 

1019 self.options['admin_passwd'] = crypt_context.hash(new_password) 

1020 

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 

1033 

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 ) 

1041 

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()))))) 

1047 

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 } 

1061 

1062 

1063config = configmanager()