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

252 statements  

1""" 

2An application for IPython. 

3 

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

5handling configuration and creating configurables. 

6 

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

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

9""" 

10 

11# Copyright (c) IPython Development Team. 

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

13 

14import atexit 

15from copy import deepcopy 

16import logging 

17import os 

18import shutil 

19import sys 

20 

21from pathlib import Path 

22 

23from traitlets.config.application import Application, catch_config_error 

24from traitlets.config.loader import ConfigFileNotFound, PyFileConfigLoader 

25from IPython.core import release, crashhandler 

26from IPython.core.profiledir import ProfileDir, ProfileDirError 

27from IPython.paths import get_ipython_dir, get_ipython_package_dir 

28from IPython.utils.path import ensure_dir_exists 

29from traitlets import ( 

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

31 default, observe, 

32) 

33 

34if os.name == "nt": 

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

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

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

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

39 else: 

40 SYSTEM_CONFIG_DIRS = [] 

41else: 

42 SYSTEM_CONFIG_DIRS = [ 

43 "/usr/local/etc/ipython", 

44 "/etc/ipython", 

45 ] 

46 

47 

48ENV_CONFIG_DIRS = [] 

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

50if _env_config_dir not in SYSTEM_CONFIG_DIRS: 

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

52 ENV_CONFIG_DIRS.append(_env_config_dir) 

53 

54 

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

56if _envvar in {None, ''}: 

57 IPYTHON_SUPPRESS_CONFIG_ERRORS = None 

58else: 

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

60 IPYTHON_SUPPRESS_CONFIG_ERRORS = True 

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

62 IPYTHON_SUPPRESS_CONFIG_ERRORS = False 

63 else: 

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

65 

66# aliases and flags 

67 

68base_aliases = {} 

69if isinstance(Application.aliases, dict): 

70 # traitlets 5 

71 base_aliases.update(Application.aliases) 

72base_aliases.update( 

73 { 

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

75 "profile": "BaseIPythonApplication.profile", 

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

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

78 "config": "BaseIPythonApplication.extra_config_file", 

79 } 

80) 

81 

82base_flags = dict() 

83if isinstance(Application.flags, dict): 

84 # traitlets 5 

85 base_flags.update(Application.flags) 

86base_flags.update( 

87 dict( 

88 debug=( 

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

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

91 ), 

92 quiet=( 

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

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

95 ), 

96 init=( 

97 { 

98 "BaseIPythonApplication": { 

99 "copy_config_files": True, 

100 "auto_create": True, 

101 } 

102 }, 

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

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

105 """, 

106 ), 

107 ) 

108) 

109 

110 

111class ProfileAwareConfigLoader(PyFileConfigLoader): 

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

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

114 if profile is not None: 

115 try: 

116 profile_dir = ProfileDir.find_profile_dir_by_name( 

117 get_ipython_dir(), 

118 profile, 

119 ) 

120 except ProfileDirError: 

121 return 

122 path = profile_dir.location 

123 return super().load_subconfig(fname, path=path) 

124 

125class BaseIPythonApplication(Application): 

126 name = "ipython" 

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

128 version = Unicode(release.version) 

129 

130 aliases = base_aliases 

131 flags = base_flags 

132 classes = List([ProfileDir]) 

133 

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

135 python_config_loader_class = ProfileAwareConfigLoader 

136 

137 # Track whether the config_file has changed, 

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

139 config_file_specified = Set() 

140 

141 config_file_name = Unicode() 

142 @default('config_file_name') 

143 def _config_file_name_default(self): 

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

145 @observe('config_file_name') 

146 def _config_file_name_changed(self, change): 

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

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

149 

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

151 builtin_profile_dir = Unicode( 

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

153 ) 

154 

155 config_file_paths = List(Unicode()) 

156 @default('config_file_paths') 

157 def _config_file_paths_default(self): 

158 return [] 

159 

160 extra_config_file = Unicode(help="""Path to an extra config file to load. 

161 

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

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

164 @observe('extra_config_file') 

165 def _extra_config_file_changed(self, change): 

166 old = change['old'] 

167 new = change['new'] 

168 try: 

169 self.config_files.remove(old) 

170 except ValueError: 

171 pass 

172 self.config_file_specified.add(new) 

173 self.config_files.append(new) 

174 

175 profile = Unicode('default', 

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

177 ).tag(config=True) 

178 

179 @observe('profile') 

180 def _profile_changed(self, change): 

181 self.builtin_profile_dir = os.path.join( 

182 get_ipython_package_dir(), 'config', 'profile', change['new'] 

183 ) 

184 

185 add_ipython_dir_to_sys_path = Bool( 

186 False, 

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

188 

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

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

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

192 but can be set to true for legacy reasons. 

193 """, 

194 ).tag(config=True) 

195 

196 ipython_dir = Unicode( 

197 help=""" 

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

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

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

201 the environment variable IPYTHONDIR. 

202 """ 

203 ).tag(config=True) 

204 @default('ipython_dir') 

205 def _ipython_dir_default(self): 

206 d = get_ipython_dir() 

207 self._ipython_dir_changed({ 

208 'name': 'ipython_dir', 

209 'old': d, 

210 'new': d, 

211 }) 

212 return d 

213 

214 _in_init_profile_dir = False 

215 

216 profile_dir = Instance(ProfileDir, allow_none=True) 

217 

218 @default('profile_dir') 

219 def _profile_dir_default(self): 

220 # avoid recursion 

221 if self._in_init_profile_dir: 

222 return 

223 # profile_dir requested early, force initialization 

224 self.init_profile_dir() 

225 return self.profile_dir 

226 

227 overwrite = Bool(False, 

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

229 ).tag(config=True) 

230 

231 auto_create = Bool(False, 

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

233 ).tag(config=True) 

234 

235 config_files = List(Unicode()) 

236 

237 @default('config_files') 

238 def _config_files_default(self): 

239 return [self.config_file_name] 

240 

241 copy_config_files = Bool(False, 

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

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

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

245 default config files will be automatically generated. 

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

247 

248 verbose_crash = Bool(False, 

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

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

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

252 

253 # The class to use as the crash handler. 

254 crash_handler_class = Type(crashhandler.CrashHandler) 

255 

256 @catch_config_error 

257 def __init__(self, **kwargs): 

258 super().__init__(**kwargs) 

259 # ensure current working directory exists 

260 try: 

261 os.getcwd() 

262 except OSError: 

263 # exit if cwd doesn't exist 

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

265 self.exit(1) 

266 

267 #------------------------------------------------------------------------- 

268 # Various stages of Application creation 

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

270 

271 def init_crash_handler(self): 

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

273 self.crash_handler = self.crash_handler_class(self) 

274 sys.excepthook = self.excepthook 

275 def unset_crashhandler(): 

276 sys.excepthook = sys.__excepthook__ 

277 atexit.register(unset_crashhandler) 

278 

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

280 """this is sys.excepthook after init_crashhandler 

281 

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

283 a regular traceback with a short message (crash_handler_lite) 

284 """ 

285 

286 if self.verbose_crash: 

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

288 else: 

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

290 

291 @observe('ipython_dir') 

292 def _ipython_dir_changed(self, change): 

293 old = change['old'] 

294 new = change['new'] 

295 if old is not Undefined: 

296 str_old = os.path.abspath(old) 

297 if str_old in sys.path: 

298 sys.path.remove(str_old) 

299 if self.add_ipython_dir_to_sys_path: 

300 str_path = os.path.abspath(new) 

301 sys.path.append(str_path) 

302 ensure_dir_exists(new) 

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

304 readme_src = os.path.join( 

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

306 ) 

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

308 shutil.copy(readme_src, readme) 

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

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

311 try: 

312 ensure_dir_exists(path) 

313 except OSError as e: 

314 # this will not be EEXIST 

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

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

317 

318 def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS): 

319 """Load the config file. 

320 

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

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

323 to False, so errors will make tests fail. 

324 

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

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

327 

328 The default value can be set : 

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

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

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

332 

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

334 """ 

335 

336 

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

338 base_config = 'ipython_config.py' 

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

340 base_config) 

341 try: 

342 if suppress_errors is not None: 

343 old_value = Application.raise_config_file_errors 

344 Application.raise_config_file_errors = not suppress_errors 

345 Application.load_config_file( 

346 self, 

347 base_config, 

348 path=self.config_file_paths 

349 ) 

350 except ConfigFileNotFound: 

351 # ignore errors loading parent 

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

353 pass 

354 if suppress_errors is not None: 

355 Application.raise_config_file_errors = old_value 

356 

357 for config_file_name in self.config_files: 

358 if not config_file_name or config_file_name == base_config: 

359 continue 

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

361 self.config_file_name) 

362 try: 

363 Application.load_config_file( 

364 self, 

365 config_file_name, 

366 path=self.config_file_paths 

367 ) 

368 except ConfigFileNotFound: 

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

370 if config_file_name in self.config_file_specified: 

371 msg = self.log.warning 

372 else: 

373 msg = self.log.debug 

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

375 except Exception: 

376 # For testing purposes. 

377 if not suppress_errors: 

378 raise 

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

380 self.config_file_name, exc_info=True) 

381 

382 def init_profile_dir(self): 

383 """initialize the profile dir""" 

384 self._in_init_profile_dir = True 

385 if self.profile_dir is not None: 

386 # already ran 

387 return 

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

389 # location not specified, find by profile name 

390 try: 

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

392 except ProfileDirError: 

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

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

395 try: 

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

397 except ProfileDirError: 

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

399 self.exit(1) 

400 else: 

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

402 else: 

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

404 self.exit(1) 

405 else: 

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

407 else: 

408 location = self.config.ProfileDir.location 

409 # location is fully specified 

410 try: 

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

412 except ProfileDirError: 

413 # not found, maybe create it 

414 if self.auto_create: 

415 try: 

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

417 except ProfileDirError: 

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

419 self.exit(1) 

420 else: 

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

422 else: 

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

424 self.exit(1) 

425 else: 

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

427 # if profile_dir is specified explicitly, set profile name 

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

429 if dir_name.startswith('profile_'): 

430 self.profile = dir_name[8:] 

431 

432 self.profile_dir = p 

433 self.config_file_paths.append(p.location) 

434 self._in_init_profile_dir = False 

435 

436 def init_config_files(self): 

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

438 self.config_file_paths.extend(ENV_CONFIG_DIRS) 

439 self.config_file_paths.extend(SYSTEM_CONFIG_DIRS) 

440 # copy config files 

441 path = Path(self.builtin_profile_dir) 

442 if self.copy_config_files: 

443 src = self.profile 

444 

445 cfg = self.config_file_name 

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

447 self.log.warning( 

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

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

450 ) 

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

452 else: 

453 self.stage_default_config_file() 

454 else: 

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

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

457 # on the first go 

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

459 for fullpath in files: 

460 cfg = fullpath.name 

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

462 # file was copied 

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

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

465 ) 

466 

467 

468 def stage_default_config_file(self): 

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

470 s = self.generate_config_file() 

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

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

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

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

475 

476 @catch_config_error 

477 def initialize(self, argv=None): 

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

479 self.parse_command_line(argv) 

480 self.init_crash_handler() 

481 if self.subapp is not None: 

482 # stop here if subapp is taking over 

483 return 

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

485 # so that it has highest priority 

486 cl_config = deepcopy(self.config) 

487 self.init_profile_dir() 

488 self.init_config_files() 

489 self.load_config_file() 

490 # enforce cl-opts override configfile opts: 

491 self.update_config(cl_config)