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