application.py
492 lines
| 18.5 KiB
| text/x-python
|
PythonLexer
|
r2185 | # encoding: utf-8 | ||
""" | ||||
|
r2301 | An application for IPython. | ||
All top-level applications should use the classes in this module for | ||||
|
r12814 | handling configuration and creating configurables. | ||
|
r2301 | |||
|
r4872 | The job of an :class:`Application` is to create the master configuration | ||
|
r2731 | object and then create the configurable objects, passing the config to them. | ||
|
r2185 | """ | ||
|
r18527 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
|
r2185 | |||
|
r4994 | import atexit | ||
|
r22585 | from copy import deepcopy | ||
|
r2252 | import logging | ||
|
r2200 | import os | ||
|
r4023 | import shutil | ||
|
r2185 | import sys | ||
|
r2245 | |||
|
r25972 | from pathlib import Path | ||
|
r21253 | from traitlets.config.application import Application, catch_config_error | ||
from traitlets.config.loader import ConfigFileNotFound, PyFileConfigLoader | ||||
|
r2403 | from IPython.core import release, crashhandler | ||
|
r4024 | from IPython.core.profiledir import ProfileDir, ProfileDirError | ||
|
r21253 | from IPython.paths import get_ipython_dir, get_ipython_package_dir | ||
from IPython.utils.path import ensure_dir_exists | ||||
|
r22340 | from traitlets import ( | ||
|
r23932 | List, Unicode, Type, Bool, Set, Instance, Undefined, | ||
|
r22340 | default, observe, | ||
) | ||||
|
r2185 | |||
|
r25972 | if os.name == "nt": | ||
|
r27343 | programdata = os.environ.get("PROGRAMDATA", None) | ||
if programdata is not None: | ||||
SYSTEM_CONFIG_DIRS = [str(Path(programdata) / "ipython")] | ||||
|
r18527 | else: # PROGRAMDATA is not defined by default on XP. | ||
SYSTEM_CONFIG_DIRS = [] | ||||
else: | ||||
SYSTEM_CONFIG_DIRS = [ | ||||
"/usr/local/etc/ipython", | ||||
"/etc/ipython", | ||||
] | ||||
|
r2185 | |||
|
r23759 | |||
ENV_CONFIG_DIRS = [] | ||||
_env_config_dir = os.path.join(sys.prefix, 'etc', 'ipython') | ||||
if _env_config_dir not in SYSTEM_CONFIG_DIRS: | ||||
# only add ENV_CONFIG if sys.prefix is not already included | ||||
ENV_CONFIG_DIRS.append(_env_config_dir) | ||||
|
r22366 | _envvar = os.environ.get('IPYTHON_SUPPRESS_CONFIG_ERRORS') | ||
|
r22360 | if _envvar in {None, ''}: | ||
|
r22366 | IPYTHON_SUPPRESS_CONFIG_ERRORS = None | ||
|
r22360 | else: | ||
if _envvar.lower() in {'1','true'}: | ||||
|
r22366 | IPYTHON_SUPPRESS_CONFIG_ERRORS = True | ||
|
r22360 | elif _envvar.lower() in {'0','false'} : | ||
|
r22366 | IPYTHON_SUPPRESS_CONFIG_ERRORS = False | ||
|
r22360 | else: | ||
|
r22366 | sys.exit("Unsupported value for environment variable: 'IPYTHON_SUPPRESS_CONFIG_ERRORS' is set to '%s' which is none of {'0', '1', 'false', 'true', ''}."% _envvar ) | ||
|
r4023 | |||
# aliases and flags | ||||
|
r26681 | base_aliases = {} | ||
if isinstance(Application.aliases, dict): | ||||
# traitlets 5 | ||||
base_aliases.update(Application.aliases) | ||||
base_aliases.update( | ||||
{ | ||||
"profile-dir": "ProfileDir.location", | ||||
"profile": "BaseIPythonApplication.profile", | ||||
"ipython-dir": "BaseIPythonApplication.ipython_dir", | ||||
"log-level": "Application.log_level", | ||||
"config": "BaseIPythonApplication.extra_config_file", | ||||
} | ||||
) | ||||
base_flags = dict() | ||||
if isinstance(Application.flags, dict): | ||||
# traitlets 5 | ||||
base_flags.update(Application.flags) | ||||
base_flags.update( | ||||
dict( | ||||
debug=( | ||||
{"Application": {"log_level": logging.DEBUG}}, | ||||
"set log level to logging.DEBUG (maximize logging output)", | ||||
), | ||||
quiet=( | ||||
{"Application": {"log_level": logging.CRITICAL}}, | ||||
"set log level to logging.CRITICAL (minimize logging output)", | ||||
), | ||||
init=( | ||||
{ | ||||
"BaseIPythonApplication": { | ||||
"copy_config_files": True, | ||||
"auto_create": True, | ||||
} | ||||
}, | ||||
"""Initialize profile with default config files. This is equivalent | ||||
|
r4247 | to running `ipython profile create <profile>` prior to startup. | ||
|
r26681 | """, | ||
), | ||||
) | ||||
|
r4023 | ) | ||
|
r26681 | |||
|
r20868 | class ProfileAwareConfigLoader(PyFileConfigLoader): | ||
"""A Python file config loader that is aware of IPython profiles.""" | ||||
def load_subconfig(self, fname, path=None, profile=None): | ||||
if profile is not None: | ||||
try: | ||||
profile_dir = ProfileDir.find_profile_dir_by_name( | ||||
get_ipython_dir(), | ||||
profile, | ||||
) | ||||
except ProfileDirError: | ||||
return | ||||
path = profile_dir.location | ||||
return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path) | ||||
|
r4023 | |||
class BaseIPythonApplication(Application): | ||||
|
r28027 | name = "ipython" | ||
description = "IPython: an enhanced interactive Python shell." | ||||
|
r4023 | version = Unicode(release.version) | ||
|
r4872 | |||
|
r23932 | aliases = base_aliases | ||
flags = base_flags | ||||
|
r4114 | classes = List([ProfileDir]) | ||
|
r20868 | |||
# enable `load_subconfig('cfg.py', profile='name')` | ||||
python_config_loader_class = ProfileAwareConfigLoader | ||||
|
r4872 | |||
|
r4023 | # Track whether the config_file has changed, | ||
# because some logic happens only if we aren't using the default. | ||||
|
r11277 | config_file_specified = Set() | ||
|
r4872 | |||
|
r11364 | config_file_name = Unicode() | ||
|
r22340 | @default('config_file_name') | ||
|
r4025 | def _config_file_name_default(self): | ||
return self.name.replace('-','_') + u'_config.py' | ||||
|
r22340 | @observe('config_file_name') | ||
def _config_file_name_changed(self, change): | ||||
if change['new'] != change['old']: | ||||
self.config_file_specified.add(change['new']) | ||||
|
r4023 | |||
# The directory that contains IPython's builtin profiles. | ||||
builtin_profile_dir = Unicode( | ||||
os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default') | ||||
) | ||||
|
r18527 | |||
|
r21534 | config_file_paths = List(Unicode()) | ||
|
r22340 | @default('config_file_paths') | ||
|
r4023 | def _config_file_paths_default(self): | ||
|
r27464 | return [] | ||
|
r4023 | |||
|
r22340 | extra_config_file = Unicode( | ||
|
r11277 | help="""Path to an extra config file to load. | ||
If specified, load this config file in addition to any other IPython config. | ||||
|
r22340 | """).tag(config=True) | ||
@observe('extra_config_file') | ||||
def _extra_config_file_changed(self, change): | ||||
old = change['old'] | ||||
new = change['new'] | ||||
|
r11277 | try: | ||
self.config_files.remove(old) | ||||
except ValueError: | ||||
pass | ||||
self.config_file_specified.add(new) | ||||
self.config_files.append(new) | ||||
|
r22340 | profile = Unicode(u'default', | ||
|
r4023 | help="""The IPython profile to use.""" | ||
|
r22340 | ).tag(config=True) | ||
@observe('profile') | ||||
def _profile_changed(self, change): | ||||
|
r4023 | self.builtin_profile_dir = os.path.join( | ||
|
r22340 | get_ipython_package_dir(), u'config', u'profile', change['new'] | ||
|
r4023 | ) | ||
|
r4872 | |||
|
r27198 | add_ipython_dir_to_sys_path = Bool( | ||
False, | ||||
|
r27190 | """Should the IPython profile directory be added to sys path ? | ||
This option was non-existing before IPython 8.0, and ipython_dir was added to | ||||
sys path to allow import of extensions present there. This was historical | ||||
baggage from when pip did not exist. This now default to false, | ||||
|
r27197 | but can be set to true for legacy reasons. | ||
|
r27198 | """, | ||
).tag(config=True) | ||||
|
r27190 | |||
|
r22340 | ipython_dir = Unicode( | ||
|
r4023 | help=""" | ||
The name of the IPython directory. This directory is used for logging | ||||
configuration (through profiles), history storage, etc. The default | ||||
|
r16597 | is usually $HOME/.ipython. This option can also be specified through | ||
|
r6696 | the environment variable IPYTHONDIR. | ||
|
r2294 | """ | ||
|
r22340 | ).tag(config=True) | ||
@default('ipython_dir') | ||||
|
r12814 | def _ipython_dir_default(self): | ||
d = get_ipython_dir() | ||||
|
r22340 | self._ipython_dir_changed({ | ||
'name': 'ipython_dir', | ||||
'old': d, | ||||
'new': d, | ||||
}) | ||||
|
r12814 | return d | ||
|
r11836 | _in_init_profile_dir = False | ||
|
r28565 | |||
|
r20946 | profile_dir = Instance(ProfileDir, allow_none=True) | ||
|
r28565 | |||
|
r22340 | @default('profile_dir') | ||
|
r11836 | def _profile_dir_default(self): | ||
# avoid recursion | ||||
if self._in_init_profile_dir: | ||||
return | ||||
# profile_dir requested early, force initialization | ||||
self.init_profile_dir() | ||||
return self.profile_dir | ||||
|
r4872 | |||
|
r22340 | overwrite = Bool(False, | ||
help="""Whether to overwrite existing config files when copying""" | ||||
).tag(config=True) | ||||
|
r28565 | |||
|
r22340 | auto_create = Bool(False, | ||
help="""Whether to create profile dir if it doesn't exist""" | ||||
).tag(config=True) | ||||
|
r4872 | |||
|
r21534 | config_files = List(Unicode()) | ||
|
r28565 | |||
|
r22340 | @default('config_files') | ||
|
r4023 | def _config_files_default(self): | ||
|
r11364 | return [self.config_file_name] | ||
|
r4872 | |||
|
r22340 | copy_config_files = Bool(False, | ||
|
r4025 | help="""Whether to install the default config files into the profile dir. | ||
If a new profile is being created, and IPython contains config files for that | ||||
profile, then they will be staged into the new directory. Otherwise, | ||||
default config files will be automatically generated. | ||||
|
r22340 | """).tag(config=True) | ||
|
r5317 | |||
|
r22340 | verbose_crash = Bool(False, | ||
|
r6872 | help="""Create a massive crash report when IPython encounters what may be an | ||
|
r5317 | internal error. The default is to append a short message to the | ||
|
r22340 | usual traceback""").tag(config=True) | ||
|
r4023 | |||
# The class to use as the crash handler. | ||||
crash_handler_class = Type(crashhandler.CrashHandler) | ||||
|
r10381 | @catch_config_error | ||
|
r4023 | def __init__(self, **kwargs): | ||
super(BaseIPythonApplication, self).__init__(**kwargs) | ||||
|
r10154 | # ensure current working directory exists | ||
|
r10169 | try: | ||
|
r23045 | os.getcwd() | ||
|
r10169 | except: | ||
|
r20485 | # exit if cwd doesn't exist | ||
|
r10169 | self.log.error("Current working directory doesn't exist.") | ||
|
r20485 | self.exit(1) | ||
|
r10154 | |||
|
r4023 | #------------------------------------------------------------------------- | ||
# Various stages of Application creation | ||||
#------------------------------------------------------------------------- | ||||
|
r21893 | |||
|
r4023 | def init_crash_handler(self): | ||
"""Create a crash handler, typically setting sys.excepthook to it.""" | ||||
self.crash_handler = self.crash_handler_class(self) | ||||
|
r5317 | sys.excepthook = self.excepthook | ||
|
r4994 | def unset_crashhandler(): | ||
sys.excepthook = sys.__excepthook__ | ||||
atexit.register(unset_crashhandler) | ||||
|
r5317 | |||
def excepthook(self, etype, evalue, tb): | ||||
"""this is sys.excepthook after init_crashhandler | ||||
|
r26491 | |||
|
r5317 | set self.verbose_crash=True to use our full crashhandler, instead of | ||
a regular traceback with a short message (crash_handler_lite) | ||||
""" | ||||
if self.verbose_crash: | ||||
return self.crash_handler(etype, evalue, tb) | ||||
else: | ||||
return crashhandler.crash_handler_lite(etype, evalue, tb) | ||||
|
r22340 | |||
@observe('ipython_dir') | ||||
def _ipython_dir_changed(self, change): | ||||
old = change['old'] | ||||
new = change['new'] | ||||
|
r21172 | if old is not Undefined: | ||
|
r23666 | str_old = os.path.abspath(old) | ||
|
r19121 | if str_old in sys.path: | ||
sys.path.remove(str_old) | ||||
|
r27190 | if self.add_ipython_dir_to_sys_path: | ||
str_path = os.path.abspath(new) | ||||
|
r27197 | sys.path.append(str_path) | ||
|
r27190 | ensure_dir_exists(new) | ||
|
r27198 | readme = os.path.join(new, "README") | ||
readme_src = os.path.join( | ||||
get_ipython_package_dir(), "config", "profile", "README" | ||||
) | ||||
|
r27190 | if not os.path.exists(readme) and os.path.exists(readme_src): | ||
shutil.copy(readme_src, readme) | ||||
|
r27198 | for d in ("extensions", "nbextensions"): | ||
|
r27190 | path = os.path.join(new, d) | ||
try: | ||||
ensure_dir_exists(path) | ||||
except OSError as e: | ||||
# this will not be EEXIST | ||||
self.log.error("couldn't create path %s: %s", path, e) | ||||
|
r28052 | self.log.debug("IPYTHONDIR set to: %s", new) | ||
|
r4023 | |||
|
r22366 | def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS): | ||
|
r2200 | """Load the config file. | ||
|
r4023 | |||
|
r3458 | By default, errors in loading config are handled, and a warning | ||
printed on screen. For testing, the suppress_errors option is set | ||||
to False, so errors will make tests fail. | ||||
|
r22360 | |||
|
r25146 | `suppress_errors` default value is to be `None` in which case the | ||
|
r22360 | behavior default to the one of `traitlets.Application`. | ||
|
r22364 | The default value can be set : | ||
|
r22366 | - to `False` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '0', or 'false' (case insensitive). | ||
- to `True` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '1' or 'true' (case insensitive). | ||||
- to `None` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '' (empty string) or leaving it unset. | ||||
|
r22360 | |||
Any other value are invalid, and will make IPython exit with a non-zero return code. | ||||
|
r2200 | """ | ||
|
r22360 | |||
|
r4564 | self.log.debug("Searching path %s for config files", self.config_file_paths) | ||
|
r4025 | base_config = 'ipython_config.py' | ||
self.log.debug("Attempting to load config file: %s" % | ||||
base_config) | ||||
try: | ||||
|
r22360 | if suppress_errors is not None: | ||
old_value = Application.raise_config_file_errors | ||||
Application.raise_config_file_errors = not suppress_errors; | ||||
|
r4025 | Application.load_config_file( | ||
self, | ||||
|
r4872 | base_config, | ||
|
r4025 | path=self.config_file_paths | ||
) | ||||
|
r4909 | except ConfigFileNotFound: | ||
|
r4025 | # ignore errors loading parent | ||
|
r4564 | self.log.debug("Config file %s not found", base_config) | ||
|
r4025 | pass | ||
|
r22360 | if suppress_errors is not None: | ||
Application.raise_config_file_errors = old_value | ||||
|
r11277 | |||
for config_file_name in self.config_files: | ||||
if not config_file_name or config_file_name == base_config: | ||||
continue | ||||
self.log.debug("Attempting to load config file: %s" % | ||||
self.config_file_name) | ||||
try: | ||||
Application.load_config_file( | ||||
self, | ||||
config_file_name, | ||||
path=self.config_file_paths | ||||
) | ||||
except ConfigFileNotFound: | ||||
# Only warn if the default config file was NOT being used. | ||||
if config_file_name in self.config_file_specified: | ||||
|
r21887 | msg = self.log.warning | ||
|
r11277 | else: | ||
msg = self.log.debug | ||||
msg("Config file not found, skipping: %s", config_file_name) | ||||
|
r21262 | except Exception: | ||
|
r11277 | # For testing purposes. | ||
if not suppress_errors: | ||||
raise | ||||
|
r21887 | self.log.warning("Error loading config file: %s" % | ||
|
r11277 | self.config_file_name, exc_info=True) | ||
|
r2294 | |||
|
r4023 | def init_profile_dir(self): | ||
"""initialize the profile dir""" | ||||
|
r11836 | self._in_init_profile_dir = True | ||
|
r11870 | if self.profile_dir is not None: | ||
# already ran | ||||
return | ||||
|
r12796 | if 'ProfileDir.location' not in self.config: | ||
|
r4023 | # location not specified, find by profile name | ||
|
r2270 | try: | ||
|
r4023 | p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config) | ||
except ProfileDirError: | ||||
# not found, maybe create it (always create default profile) | ||||
|
r8885 | if self.auto_create or self.profile == 'default': | ||
|
r4023 | try: | ||
p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config) | ||||
except ProfileDirError: | ||||
self.log.fatal("Could not create profile: %r"%self.profile) | ||||
self.exit(1) | ||||
else: | ||||
self.log.info("Created profile dir: %r"%p.location) | ||||
else: | ||||
self.log.fatal("Profile %r not found."%self.profile) | ||||
self.exit(1) | ||||
else: | ||||
|
r28051 | self.log.debug("Using existing profile dir: %r", p.location) | ||
|
r2303 | else: | ||
|
r12796 | location = self.config.ProfileDir.location | ||
|
r4023 | # location is fully specified | ||
try: | ||||
p = ProfileDir.find_profile_dir(location, self.config) | ||||
except ProfileDirError: | ||||
# not found, maybe create it | ||||
if self.auto_create: | ||||
try: | ||||
p = ProfileDir.create_profile_dir(location, self.config) | ||||
except ProfileDirError: | ||||
self.log.fatal("Could not create profile directory: %r"%location) | ||||
self.exit(1) | ||||
else: | ||||
|
r20725 | self.log.debug("Creating new profile dir: %r"%location) | ||
|
r4023 | else: | ||
self.log.fatal("Profile directory %r not found."%location) | ||||
self.exit(1) | ||||
else: | ||||
|
r28051 | self.log.debug("Using existing profile dir: %r", p.location) | ||
|
r14790 | # if profile_dir is specified explicitly, set profile name | ||
dir_name = os.path.basename(p.location) | ||||
if dir_name.startswith('profile_'): | ||||
self.profile = dir_name[8:] | ||||
|
r4872 | |||
|
r4023 | self.profile_dir = p | ||
self.config_file_paths.append(p.location) | ||||
|
r11836 | self._in_init_profile_dir = False | ||
|
r4872 | |||
|
r4023 | def init_config_files(self): | ||
"""[optionally] copy default config files into profile dir.""" | ||||
|
r23759 | self.config_file_paths.extend(ENV_CONFIG_DIRS) | ||
|
r18527 | self.config_file_paths.extend(SYSTEM_CONFIG_DIRS) | ||
|
r4023 | # copy config files | ||
|
r25972 | path = Path(self.builtin_profile_dir) | ||
|
r4023 | if self.copy_config_files: | ||
src = self.profile | ||||
|
r4872 | |||
|
r4025 | cfg = self.config_file_name | ||
|
r25972 | if path and (path / cfg).exists(): | ||
self.log.warning( | ||||
"Staging %r from %s into %r [overwrite=%s]" | ||||
% (cfg, src, self.profile_dir.location, self.overwrite) | ||||
|
r4025 | ) | ||
|
r4023 | self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite) | ||
|
r4025 | else: | ||
self.stage_default_config_file() | ||||
|
r4122 | else: | ||
# Still stage *bundled* config files, but not generated ones | ||||
# This is necessary for `ipython profile=sympy` to load the profile | ||||
# on the first go | ||||
|
r25972 | files = path.glob("*.py") | ||
|
r4122 | for fullpath in files: | ||
|
r25972 | cfg = fullpath.name | ||
|
r4122 | if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False): | ||
# file was copied | ||||
|
r21887 | self.log.warning("Staging bundled %s from %s into %r"%( | ||
|
r4122 | cfg, self.profile, self.profile_dir.location) | ||
) | ||||
|
r4872 | |||
|
r4025 | def stage_default_config_file(self): | ||
"""auto generate default config file, and stage it into the profile.""" | ||||
s = self.generate_config_file() | ||||
|
r25972 | config_file = Path(self.profile_dir.location) / self.config_file_name | ||
if self.overwrite or not config_file.exists(): | ||||
|
r28052 | self.log.warning("Generating default config file: %r", (config_file)) | ||
|
r27495 | config_file.write_text(s, encoding="utf-8") | ||
|
r4872 | |||
|
r5214 | @catch_config_error | ||
|
r4023 | def initialize(self, argv=None): | ||
|
r4106 | # don't hook up crash handler before parsing command-line | ||
|
r4023 | self.parse_command_line(argv) | ||
|
r4106 | self.init_crash_handler() | ||
|
r4025 | if self.subapp is not None: | ||
# stop here if subapp is taking over | ||||
return | ||||
|
r22585 | # save a copy of CLI config to re-load after config files | ||
# so that it has highest priority | ||||
cl_config = deepcopy(self.config) | ||||
|
r4023 | self.init_profile_dir() | ||
self.init_config_files() | ||||
self.load_config_file() | ||||
# enforce cl-opts override configfile opts: | ||||
self.update_config(cl_config) | ||||