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