Format: text/x-python
⬇ Download File (8.3 KB)
   1  # encoding: utf-8
   2  """An object for managing IPython profile directories."""
   3  
   4  # Copyright (c) IPython Development Team.
   5  # Distributed under the terms of the Modified BSD License.
   6  
   7  import os
   8  import shutil
   9  import errno
  10  from pathlib import Path
  11  
  12  from traitlets.config.configurable import LoggingConfigurable
  13  from ..paths import get_ipython_package_dir
  14  from ..utils.path import expand_path, ensure_dir_exists
  15  from traitlets import Unicode, Bool, observe
  16  
  17  from typing import Optional
  18  
  19  #-----------------------------------------------------------------------------
  20  # Module errors
  21  #-----------------------------------------------------------------------------
  22  
  23  class ProfileDirError(Exception):
  24      pass
  25  
  26  
  27  #-----------------------------------------------------------------------------
  28  # Class for managing profile directories
  29  #-----------------------------------------------------------------------------
  30  
  31  class ProfileDir(LoggingConfigurable):
  32      """An object to manage the profile directory and its resources.
  33  
  34      The profile directory is used by all IPython applications, to manage
  35      configuration, logging and security.
  36  
  37      This object knows how to find, create and manage these directories. This
  38      should be used by any code that wants to handle profiles.
  39      """
  40  
  41      security_dir_name = Unicode('security')
  42      log_dir_name = Unicode('log')
  43      startup_dir_name = Unicode('startup')
  44      pid_dir_name = Unicode('pid')
  45      static_dir_name = Unicode('static')
  46      security_dir = Unicode(u'')
  47      log_dir = Unicode(u'')
  48      startup_dir = Unicode(u'')
  49      pid_dir = Unicode(u'')
  50      static_dir = Unicode(u'')
  51  
  52      location = Unicode(u'',
  53          help="""Set the profile location directly. This overrides the logic used by the
  54          `profile` option.""",
  55          ).tag(config=True)
  56  
  57      _location_isset = Bool(False) # flag for detecting multiply set location
  58      @observe('location')
  59      def _location_changed(self, change):
  60          if self._location_isset:
  61              raise RuntimeError("Cannot set profile location more than once.")
  62          self._location_isset = True
  63          new = change['new']
  64          ensure_dir_exists(new)
  65  
  66          # ensure config files exist:
  67          self.security_dir = os.path.join(new, self.security_dir_name)
  68          self.log_dir = os.path.join(new, self.log_dir_name)
  69          self.startup_dir = os.path.join(new, self.startup_dir_name)
  70          self.pid_dir = os.path.join(new, self.pid_dir_name)
  71          self.static_dir = os.path.join(new, self.static_dir_name)
  72          self.check_dirs()
  73  
  74      def _mkdir(self, path: str, mode: Optional[int] = None) -> bool:
  75          """ensure a directory exists at a given path
  76  
  77          This is a version of os.mkdir, with the following differences:
  78  
  79          - returns whether the directory has been created or not.
  80          - ignores EEXIST, protecting against race conditions where
  81            the dir may have been created in between the check and
  82            the creation
  83          - sets permissions if requested and the dir already exists
  84  
  85          Parameters
  86          ----------
  87          path: str
  88              path of the dir to create
  89          mode: int
  90              see `mode` of `os.mkdir`
  91  
  92          Returns
  93          -------
  94          bool:
  95              returns True if it created the directory, False otherwise
  96          """
  97  
  98          if os.path.exists(path):
  99              if mode and os.stat(path).st_mode != mode:
 100                  try:
 101                      os.chmod(path, mode)
 102                  except OSError:
 103                      self.log.warning(
 104                          "Could not set permissions on %s",
 105                          path
 106                      )
 107              return False
 108          try:
 109              if mode:
 110                  os.mkdir(path, mode)
 111              else:
 112                  os.mkdir(path)
 113          except OSError as e:
 114              if e.errno == errno.EEXIST:
 115                  return False
 116              else:
 117                  raise
 118  
 119          return True
 120      
 121      @observe('log_dir')
 122      def check_log_dir(self, change=None):
 123          self._mkdir(self.log_dir)
 124      
 125      @observe('startup_dir')
 126      def check_startup_dir(self, change=None):
 127          if self._mkdir(self.startup_dir):
 128              readme = os.path.join(self.startup_dir, "README")
 129              src = os.path.join(
 130                  get_ipython_package_dir(), "core", "profile", "README_STARTUP"
 131              )
 132  
 133              if os.path.exists(src):
 134                  if not os.path.exists(readme):
 135                      shutil.copy(src, readme)
 136              else:
 137                  self.log.warning(
 138                      "Could not copy README_STARTUP to startup dir. Source file %s does not exist.",
 139                      src,
 140                  )
 141  
 142      @observe('security_dir')
 143      def check_security_dir(self, change=None):
 144          self._mkdir(self.security_dir, 0o40700)
 145  
 146      @observe('pid_dir')
 147      def check_pid_dir(self, change=None):
 148          self._mkdir(self.pid_dir, 0o40700)
 149  
 150      def check_dirs(self):
 151          self.check_security_dir()
 152          self.check_log_dir()
 153          self.check_pid_dir()
 154          self.check_startup_dir()
 155  
 156      def copy_config_file(self, config_file: str, path: Path, overwrite=False) -> bool:
 157          """Copy a default config file into the active profile directory.
 158  
 159          Default configuration files are kept in :mod:`IPython.core.profile`.
 160          This function moves these from that location to the working profile
 161          directory.
 162          """
 163          dst = Path(os.path.join(self.location, config_file))
 164          if dst.exists() and not overwrite:
 165              return False
 166          if path is None:
 167              path = os.path.join(get_ipython_package_dir(), u'core', u'profile', u'default')
 168          assert isinstance(path, Path)
 169          src = path / config_file
 170          shutil.copy(src, dst)
 171          return True
 172  
 173      @classmethod
 174      def create_profile_dir(cls, profile_dir, config=None):
 175          """Create a new profile directory given a full path.
 176  
 177          Parameters
 178          ----------
 179          profile_dir : str
 180              The full path to the profile directory.  If it does exist, it will
 181              be used.  If not, it will be created.
 182          """
 183          return cls(location=profile_dir, config=config)
 184  
 185      @classmethod
 186      def create_profile_dir_by_name(cls, path, name=u'default', config=None):
 187          """Create a profile dir by profile name and path.
 188  
 189          Parameters
 190          ----------
 191          path : unicode
 192              The path (directory) to put the profile directory in.
 193          name : unicode
 194              The name of the profile.  The name of the profile directory will
 195              be "profile_<profile>".
 196          """
 197          if not os.path.isdir(path):
 198              raise ProfileDirError('Directory not found: %s' % path)
 199          profile_dir = os.path.join(path, u'profile_' + name)
 200          return cls(location=profile_dir, config=config)
 201  
 202      @classmethod
 203      def find_profile_dir_by_name(cls, ipython_dir, name=u'default', config=None):
 204          """Find an existing profile dir by profile name, return its ProfileDir.
 205  
 206          This searches through a sequence of paths for a profile dir.  If it
 207          is not found, a :class:`ProfileDirError` exception will be raised.
 208  
 209          The search path algorithm is:
 210          1. ``os.getcwd()`` # removed for security reason.
 211          2. ``ipython_dir``
 212  
 213          Parameters
 214          ----------
 215          ipython_dir : unicode or str
 216              The IPython directory to use.
 217          name : unicode or str
 218              The name of the profile.  The name of the profile directory
 219              will be "profile_<profile>".
 220          """
 221          dirname = u'profile_' + name
 222          paths = [ipython_dir]
 223          for p in paths:
 224              profile_dir = os.path.join(p, dirname)
 225              if os.path.isdir(profile_dir):
 226                  return cls(location=profile_dir, config=config)
 227          else:
 228              raise ProfileDirError('Profile directory not found in paths: %s' % dirname)
 229  
 230      @classmethod
 231      def find_profile_dir(cls, profile_dir, config=None):
 232          """Find/create a profile dir and return its ProfileDir.
 233  
 234          This will create the profile directory if it doesn't exist.
 235  
 236          Parameters
 237          ----------
 238          profile_dir : unicode or str
 239              The path of the profile directory.
 240          """
 241          profile_dir = expand_path(profile_dir)
 242          if not os.path.isdir(profile_dir):
 243              raise ProfileDirError('Profile directory not found: %s' % profile_dir)
 244          return cls(location=profile_dir, config=config)
 245