Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/IPython/core/application.py: 34%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

253 statements  

1# encoding: utf-8 

2""" 

3An application for IPython. 

4 

5All top-level applications should use the classes in this module for 

6handling configuration and creating configurables. 

7 

8The job of an :class:`Application` is to create the master configuration 

9object and then create the configurable objects, passing the config to them. 

10""" 

11 

12# Copyright (c) IPython Development Team. 

13# Distributed under the terms of the Modified BSD License. 

14 

15import atexit 

16from copy import deepcopy 

17import logging 

18import os 

19import shutil 

20import sys 

21 

22from pathlib import Path 

23 

24from traitlets.config.application import Application, catch_config_error 

25from traitlets.config.loader import ConfigFileNotFound, PyFileConfigLoader 

26from IPython.core import release, crashhandler 

27from IPython.core.profiledir import ProfileDir, ProfileDirError 

28from IPython.paths import get_ipython_dir, get_ipython_package_dir 

29from IPython.utils.path import ensure_dir_exists 

30from traitlets import ( 

31 List, Unicode, Type, Bool, Set, Instance, Undefined, 

32 default, observe, 

33) 

34 

35if os.name == "nt": 

36 # %PROGRAMDATA% is not safe by default, require opt-in to trust it 

37 programdata = os.environ.get("PROGRAMDATA", None) 

38 if os.environ.get("IPYTHON_USE_PROGRAMDATA") == "1" and programdata is not None: 

39 SYSTEM_CONFIG_DIRS = [str(Path(programdata) / "ipython")] 

40 else: 

41 SYSTEM_CONFIG_DIRS = [] 

42else: 

43 SYSTEM_CONFIG_DIRS = [ 

44 "/usr/local/etc/ipython", 

45 "/etc/ipython", 

46 ] 

47 

48 

49ENV_CONFIG_DIRS = [] 

50_env_config_dir = os.path.join(sys.prefix, 'etc', 'ipython') 

51if _env_config_dir not in SYSTEM_CONFIG_DIRS: 

52 # only add ENV_CONFIG if sys.prefix is not already included 

53 ENV_CONFIG_DIRS.append(_env_config_dir) 

54 

55 

56_envvar = os.environ.get('IPYTHON_SUPPRESS_CONFIG_ERRORS') 

57if _envvar in {None, ''}: 

58 IPYTHON_SUPPRESS_CONFIG_ERRORS = None 

59else: 

60 if _envvar.lower() in {'1','true'}: 

61 IPYTHON_SUPPRESS_CONFIG_ERRORS = True 

62 elif _envvar.lower() in {'0','false'} : 

63 IPYTHON_SUPPRESS_CONFIG_ERRORS = False 

64 else: 

65 sys.exit("Unsupported value for environment variable: 'IPYTHON_SUPPRESS_CONFIG_ERRORS' is set to '%s' which is none of {'0', '1', 'false', 'true', ''}."% _envvar ) 

66 

67# aliases and flags 

68 

69base_aliases = {} 

70if isinstance(Application.aliases, dict): 

71 # traitlets 5 

72 base_aliases.update(Application.aliases) 

73base_aliases.update( 

74 { 

75 "profile-dir": "ProfileDir.location", 

76 "profile": "BaseIPythonApplication.profile", 

77 "ipython-dir": "BaseIPythonApplication.ipython_dir", 

78 "log-level": "Application.log_level", 

79 "config": "BaseIPythonApplication.extra_config_file", 

80 } 

81) 

82 

83base_flags = dict() 

84if isinstance(Application.flags, dict): 

85 # traitlets 5 

86 base_flags.update(Application.flags) 

87base_flags.update( 

88 dict( 

89 debug=( 

90 {"Application": {"log_level": logging.DEBUG}}, 

91 "set log level to logging.DEBUG (maximize logging output)", 

92 ), 

93 quiet=( 

94 {"Application": {"log_level": logging.CRITICAL}}, 

95 "set log level to logging.CRITICAL (minimize logging output)", 

96 ), 

97 init=( 

98 { 

99 "BaseIPythonApplication": { 

100 "copy_config_files": True, 

101 "auto_create": True, 

102 } 

103 }, 

104 """Initialize profile with default config files. This is equivalent 

105 to running `ipython profile create <profile>` prior to startup. 

106 """, 

107 ), 

108 ) 

109) 

110 

111 

112class ProfileAwareConfigLoader(PyFileConfigLoader): 

113 """A Python file config loader that is aware of IPython profiles.""" 

114 def load_subconfig(self, fname, path=None, profile=None): 

115 if profile is not None: 

116 try: 

117 profile_dir = ProfileDir.find_profile_dir_by_name( 

118 get_ipython_dir(), 

119 profile, 

120 ) 

121 except ProfileDirError: 

122 return 

123 path = profile_dir.location 

124 return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path) 

125 

126class BaseIPythonApplication(Application): 

127 name = "ipython" 

128 description = "IPython: an enhanced interactive Python shell." 

129 version = Unicode(release.version) 

130 

131 aliases = base_aliases 

132 flags = base_flags 

133 classes = List([ProfileDir]) 

134 

135 # enable `load_subconfig('cfg.py', profile='name')` 

136 python_config_loader_class = ProfileAwareConfigLoader 

137 

138 # Track whether the config_file has changed, 

139 # because some logic happens only if we aren't using the default. 

140 config_file_specified = Set() 

141 

142 config_file_name = Unicode() 

143 @default('config_file_name') 

144 def _config_file_name_default(self): 

145 return self.name.replace('-','_') + u'_config.py' 

146 @observe('config_file_name') 

147 def _config_file_name_changed(self, change): 

148 if change['new'] != change['old']: 

149 self.config_file_specified.add(change['new']) 

150 

151 # The directory that contains IPython's builtin profiles. 

152 builtin_profile_dir = Unicode( 

153 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default') 

154 ) 

155 

156 config_file_paths = List(Unicode()) 

157 @default('config_file_paths') 

158 def _config_file_paths_default(self): 

159 return [] 

160 

161 extra_config_file = Unicode( 

162 help="""Path to an extra config file to load. 

163  

164 If specified, load this config file in addition to any other IPython config. 

165 """).tag(config=True) 

166 @observe('extra_config_file') 

167 def _extra_config_file_changed(self, change): 

168 old = change['old'] 

169 new = change['new'] 

170 try: 

171 self.config_files.remove(old) 

172 except ValueError: 

173 pass 

174 self.config_file_specified.add(new) 

175 self.config_files.append(new) 

176 

177 profile = Unicode(u'default', 

178 help="""The IPython profile to use.""" 

179 ).tag(config=True) 

180 

181 @observe('profile') 

182 def _profile_changed(self, change): 

183 self.builtin_profile_dir = os.path.join( 

184 get_ipython_package_dir(), u'config', u'profile', change['new'] 

185 ) 

186 

187 add_ipython_dir_to_sys_path = Bool( 

188 False, 

189 """Should the IPython profile directory be added to sys path ? 

190 

191 This option was non-existing before IPython 8.0, and ipython_dir was added to 

192 sys path to allow import of extensions present there. This was historical 

193 baggage from when pip did not exist. This now default to false, 

194 but can be set to true for legacy reasons. 

195 """, 

196 ).tag(config=True) 

197 

198 ipython_dir = Unicode( 

199 help=""" 

200 The name of the IPython directory. This directory is used for logging 

201 configuration (through profiles), history storage, etc. The default 

202 is usually $HOME/.ipython. This option can also be specified through 

203 the environment variable IPYTHONDIR. 

204 """ 

205 ).tag(config=True) 

206 @default('ipython_dir') 

207 def _ipython_dir_default(self): 

208 d = get_ipython_dir() 

209 self._ipython_dir_changed({ 

210 'name': 'ipython_dir', 

211 'old': d, 

212 'new': d, 

213 }) 

214 return d 

215 

216 _in_init_profile_dir = False 

217 

218 profile_dir = Instance(ProfileDir, allow_none=True) 

219 

220 @default('profile_dir') 

221 def _profile_dir_default(self): 

222 # avoid recursion 

223 if self._in_init_profile_dir: 

224 return 

225 # profile_dir requested early, force initialization 

226 self.init_profile_dir() 

227 return self.profile_dir 

228 

229 overwrite = Bool(False, 

230 help="""Whether to overwrite existing config files when copying""" 

231 ).tag(config=True) 

232 

233 auto_create = Bool(False, 

234 help="""Whether to create profile dir if it doesn't exist""" 

235 ).tag(config=True) 

236 

237 config_files = List(Unicode()) 

238 

239 @default('config_files') 

240 def _config_files_default(self): 

241 return [self.config_file_name] 

242 

243 copy_config_files = Bool(False, 

244 help="""Whether to install the default config files into the profile dir. 

245 If a new profile is being created, and IPython contains config files for that 

246 profile, then they will be staged into the new directory. Otherwise, 

247 default config files will be automatically generated. 

248 """).tag(config=True) 

249 

250 verbose_crash = Bool(False, 

251 help="""Create a massive crash report when IPython encounters what may be an 

252 internal error. The default is to append a short message to the 

253 usual traceback""").tag(config=True) 

254 

255 # The class to use as the crash handler. 

256 crash_handler_class = Type(crashhandler.CrashHandler) 

257 

258 @catch_config_error 

259 def __init__(self, **kwargs): 

260 super(BaseIPythonApplication, self).__init__(**kwargs) 

261 # ensure current working directory exists 

262 try: 

263 os.getcwd() 

264 except: 

265 # exit if cwd doesn't exist 

266 self.log.error("Current working directory doesn't exist.") 

267 self.exit(1) 

268 

269 #------------------------------------------------------------------------- 

270 # Various stages of Application creation 

271 #------------------------------------------------------------------------- 

272 

273 def init_crash_handler(self): 

274 """Create a crash handler, typically setting sys.excepthook to it.""" 

275 self.crash_handler = self.crash_handler_class(self) 

276 sys.excepthook = self.excepthook 

277 def unset_crashhandler(): 

278 sys.excepthook = sys.__excepthook__ 

279 atexit.register(unset_crashhandler) 

280 

281 def excepthook(self, etype, evalue, tb): 

282 """this is sys.excepthook after init_crashhandler 

283 

284 set self.verbose_crash=True to use our full crashhandler, instead of 

285 a regular traceback with a short message (crash_handler_lite) 

286 """ 

287 

288 if self.verbose_crash: 

289 return self.crash_handler(etype, evalue, tb) 

290 else: 

291 return crashhandler.crash_handler_lite(etype, evalue, tb) 

292 

293 @observe('ipython_dir') 

294 def _ipython_dir_changed(self, change): 

295 old = change['old'] 

296 new = change['new'] 

297 if old is not Undefined: 

298 str_old = os.path.abspath(old) 

299 if str_old in sys.path: 

300 sys.path.remove(str_old) 

301 if self.add_ipython_dir_to_sys_path: 

302 str_path = os.path.abspath(new) 

303 sys.path.append(str_path) 

304 ensure_dir_exists(new) 

305 readme = os.path.join(new, "README") 

306 readme_src = os.path.join( 

307 get_ipython_package_dir(), "config", "profile", "README" 

308 ) 

309 if not os.path.exists(readme) and os.path.exists(readme_src): 

310 shutil.copy(readme_src, readme) 

311 for d in ("extensions", "nbextensions"): 

312 path = os.path.join(new, d) 

313 try: 

314 ensure_dir_exists(path) 

315 except OSError as e: 

316 # this will not be EEXIST 

317 self.log.error("couldn't create path %s: %s", path, e) 

318 self.log.debug("IPYTHONDIR set to: %s", new) 

319 

320 def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS): 

321 """Load the config file. 

322 

323 By default, errors in loading config are handled, and a warning 

324 printed on screen. For testing, the suppress_errors option is set 

325 to False, so errors will make tests fail. 

326 

327 `suppress_errors` default value is to be `None` in which case the 

328 behavior default to the one of `traitlets.Application`. 

329 

330 The default value can be set : 

331 - to `False` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '0', or 'false' (case insensitive). 

332 - to `True` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '1' or 'true' (case insensitive). 

333 - to `None` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '' (empty string) or leaving it unset. 

334 

335 Any other value are invalid, and will make IPython exit with a non-zero return code. 

336 """ 

337 

338 

339 self.log.debug("Searching path %s for config files", self.config_file_paths) 

340 base_config = 'ipython_config.py' 

341 self.log.debug("Attempting to load config file: %s" % 

342 base_config) 

343 try: 

344 if suppress_errors is not None: 

345 old_value = Application.raise_config_file_errors 

346 Application.raise_config_file_errors = not suppress_errors 

347 Application.load_config_file( 

348 self, 

349 base_config, 

350 path=self.config_file_paths 

351 ) 

352 except ConfigFileNotFound: 

353 # ignore errors loading parent 

354 self.log.debug("Config file %s not found", base_config) 

355 pass 

356 if suppress_errors is not None: 

357 Application.raise_config_file_errors = old_value 

358 

359 for config_file_name in self.config_files: 

360 if not config_file_name or config_file_name == base_config: 

361 continue 

362 self.log.debug("Attempting to load config file: %s" % 

363 self.config_file_name) 

364 try: 

365 Application.load_config_file( 

366 self, 

367 config_file_name, 

368 path=self.config_file_paths 

369 ) 

370 except ConfigFileNotFound: 

371 # Only warn if the default config file was NOT being used. 

372 if config_file_name in self.config_file_specified: 

373 msg = self.log.warning 

374 else: 

375 msg = self.log.debug 

376 msg("Config file not found, skipping: %s", config_file_name) 

377 except Exception: 

378 # For testing purposes. 

379 if not suppress_errors: 

380 raise 

381 self.log.warning("Error loading config file: %s" % 

382 self.config_file_name, exc_info=True) 

383 

384 def init_profile_dir(self): 

385 """initialize the profile dir""" 

386 self._in_init_profile_dir = True 

387 if self.profile_dir is not None: 

388 # already ran 

389 return 

390 if 'ProfileDir.location' not in self.config: 

391 # location not specified, find by profile name 

392 try: 

393 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config) 

394 except ProfileDirError: 

395 # not found, maybe create it (always create default profile) 

396 if self.auto_create or self.profile == 'default': 

397 try: 

398 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config) 

399 except ProfileDirError: 

400 self.log.fatal("Could not create profile: %r"%self.profile) 

401 self.exit(1) 

402 else: 

403 self.log.info("Created profile dir: %r"%p.location) 

404 else: 

405 self.log.fatal("Profile %r not found."%self.profile) 

406 self.exit(1) 

407 else: 

408 self.log.debug("Using existing profile dir: %r", p.location) 

409 else: 

410 location = self.config.ProfileDir.location 

411 # location is fully specified 

412 try: 

413 p = ProfileDir.find_profile_dir(location, self.config) 

414 except ProfileDirError: 

415 # not found, maybe create it 

416 if self.auto_create: 

417 try: 

418 p = ProfileDir.create_profile_dir(location, self.config) 

419 except ProfileDirError: 

420 self.log.fatal("Could not create profile directory: %r"%location) 

421 self.exit(1) 

422 else: 

423 self.log.debug("Creating new profile dir: %r"%location) 

424 else: 

425 self.log.fatal("Profile directory %r not found."%location) 

426 self.exit(1) 

427 else: 

428 self.log.debug("Using existing profile dir: %r", p.location) 

429 # if profile_dir is specified explicitly, set profile name 

430 dir_name = os.path.basename(p.location) 

431 if dir_name.startswith('profile_'): 

432 self.profile = dir_name[8:] 

433 

434 self.profile_dir = p 

435 self.config_file_paths.append(p.location) 

436 self._in_init_profile_dir = False 

437 

438 def init_config_files(self): 

439 """[optionally] copy default config files into profile dir.""" 

440 self.config_file_paths.extend(ENV_CONFIG_DIRS) 

441 self.config_file_paths.extend(SYSTEM_CONFIG_DIRS) 

442 # copy config files 

443 path = Path(self.builtin_profile_dir) 

444 if self.copy_config_files: 

445 src = self.profile 

446 

447 cfg = self.config_file_name 

448 if path and (path / cfg).exists(): 

449 self.log.warning( 

450 "Staging %r from %s into %r [overwrite=%s]" 

451 % (cfg, src, self.profile_dir.location, self.overwrite) 

452 ) 

453 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite) 

454 else: 

455 self.stage_default_config_file() 

456 else: 

457 # Still stage *bundled* config files, but not generated ones 

458 # This is necessary for `ipython profile=sympy` to load the profile 

459 # on the first go 

460 files = path.glob("*.py") 

461 for fullpath in files: 

462 cfg = fullpath.name 

463 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False): 

464 # file was copied 

465 self.log.warning("Staging bundled %s from %s into %r"%( 

466 cfg, self.profile, self.profile_dir.location) 

467 ) 

468 

469 

470 def stage_default_config_file(self): 

471 """auto generate default config file, and stage it into the profile.""" 

472 s = self.generate_config_file() 

473 config_file = Path(self.profile_dir.location) / self.config_file_name 

474 if self.overwrite or not config_file.exists(): 

475 self.log.warning("Generating default config file: %r", (config_file)) 

476 config_file.write_text(s, encoding="utf-8") 

477 

478 @catch_config_error 

479 def initialize(self, argv=None): 

480 # don't hook up crash handler before parsing command-line 

481 self.parse_command_line(argv) 

482 self.init_crash_handler() 

483 if self.subapp is not None: 

484 # stop here if subapp is taking over 

485 return 

486 # save a copy of CLI config to re-load after config files 

487 # so that it has highest priority 

488 cl_config = deepcopy(self.config) 

489 self.init_profile_dir() 

490 self.init_config_files() 

491 self.load_config_file() 

492 # enforce cl-opts override configfile opts: 

493 self.update_config(cl_config)