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 
    7import os 
    8import shutil 
    9import errno 
    10from pathlib import Path 
    11 
    12from traitlets.config.configurable import LoggingConfigurable 
    13from ..paths import get_ipython_package_dir 
    14from ..utils.path import expand_path, ensure_dir_exists 
    15from traitlets import Unicode, Bool, observe 
    16 
    17from typing import Optional 
    18 
    19#----------------------------------------------------------------------------- 
    20# Module errors 
    21#----------------------------------------------------------------------------- 
    22 
    23class ProfileDirError(Exception): 
    24    pass 
    25 
    26 
    27#----------------------------------------------------------------------------- 
    28# Class for managing profile directories 
    29#----------------------------------------------------------------------------- 
    30 
    31class 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)