##// END OF EJS Templates
Fix for non-ascii characters in IPYTHONDIR, + unit test.
Thomas Kluyver -
Show More
@@ -1,374 +1,375 b''
1 1 # -*- coding: utf-8 -*-
2 2 # coding: utf-8
3 3 """A simple configuration system.
4 4
5 5 Authors
6 6 -------
7 7 * Brian Granger
8 8 * Fernando Perez
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2008-2009 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is in
15 15 # the file COPYING, distributed as part of this software.
16 16 #-----------------------------------------------------------------------------
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 import __builtin__
23 23 import os
24 24 import sys
25 25
26 26 from IPython.external import argparse
27 27 from IPython.utils.path import filefind
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Exceptions
31 31 #-----------------------------------------------------------------------------
32 32
33 33
34 34 class ConfigError(Exception):
35 35 pass
36 36
37 37
38 38 class ConfigLoaderError(ConfigError):
39 39 pass
40 40
41 41 #-----------------------------------------------------------------------------
42 42 # Argparse fix
43 43 #-----------------------------------------------------------------------------
44 44
45 45 # Unfortunately argparse by default prints help messages to stderr instead of
46 46 # stdout. This makes it annoying to capture long help screens at the command
47 47 # line, since one must know how to pipe stderr, which many users don't know how
48 48 # to do. So we override the print_help method with one that defaults to
49 49 # stdout and use our class instead.
50 50
51 51 class ArgumentParser(argparse.ArgumentParser):
52 52 """Simple argparse subclass that prints help to stdout by default."""
53 53
54 54 def print_help(self, file=None):
55 55 if file is None:
56 56 file = sys.stdout
57 57 return super(ArgumentParser, self).print_help(file)
58 58
59 59 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Config class for holding config information
63 63 #-----------------------------------------------------------------------------
64 64
65 65
66 66 class Config(dict):
67 67 """An attribute based dict that can do smart merges."""
68 68
69 69 def __init__(self, *args, **kwds):
70 70 dict.__init__(self, *args, **kwds)
71 71 # This sets self.__dict__ = self, but it has to be done this way
72 72 # because we are also overriding __setattr__.
73 73 dict.__setattr__(self, '__dict__', self)
74 74
75 75 def _merge(self, other):
76 76 to_update = {}
77 77 for k, v in other.iteritems():
78 78 if not self.has_key(k):
79 79 to_update[k] = v
80 80 else: # I have this key
81 81 if isinstance(v, Config):
82 82 # Recursively merge common sub Configs
83 83 self[k]._merge(v)
84 84 else:
85 85 # Plain updates for non-Configs
86 86 to_update[k] = v
87 87
88 88 self.update(to_update)
89 89
90 90 def _is_section_key(self, key):
91 91 if key[0].upper()==key[0] and not key.startswith('_'):
92 92 return True
93 93 else:
94 94 return False
95 95
96 96 def __contains__(self, key):
97 97 if self._is_section_key(key):
98 98 return True
99 99 else:
100 100 return super(Config, self).__contains__(key)
101 101 # .has_key is deprecated for dictionaries.
102 102 has_key = __contains__
103 103
104 104 def _has_section(self, key):
105 105 if self._is_section_key(key):
106 106 if super(Config, self).__contains__(key):
107 107 return True
108 108 return False
109 109
110 110 def copy(self):
111 111 return type(self)(dict.copy(self))
112 112
113 113 def __copy__(self):
114 114 return self.copy()
115 115
116 116 def __deepcopy__(self, memo):
117 117 import copy
118 118 return type(self)(copy.deepcopy(self.items()))
119 119
120 120 def __getitem__(self, key):
121 121 # Because we use this for an exec namespace, we need to delegate
122 122 # the lookup of names in __builtin__ to itself. This means
123 123 # that you can't have section or attribute names that are
124 124 # builtins.
125 125 try:
126 126 return getattr(__builtin__, key)
127 127 except AttributeError:
128 128 pass
129 129 if self._is_section_key(key):
130 130 try:
131 131 return dict.__getitem__(self, key)
132 132 except KeyError:
133 133 c = Config()
134 134 dict.__setitem__(self, key, c)
135 135 return c
136 136 else:
137 137 return dict.__getitem__(self, key)
138 138
139 139 def __setitem__(self, key, value):
140 140 # Don't allow names in __builtin__ to be modified.
141 141 if hasattr(__builtin__, key):
142 142 raise ConfigError('Config variable names cannot have the same name '
143 143 'as a Python builtin: %s' % key)
144 144 if self._is_section_key(key):
145 145 if not isinstance(value, Config):
146 146 raise ValueError('values whose keys begin with an uppercase '
147 147 'char must be Config instances: %r, %r' % (key, value))
148 148 else:
149 149 dict.__setitem__(self, key, value)
150 150
151 151 def __getattr__(self, key):
152 152 try:
153 153 return self.__getitem__(key)
154 154 except KeyError, e:
155 155 raise AttributeError(e)
156 156
157 157 def __setattr__(self, key, value):
158 158 try:
159 159 self.__setitem__(key, value)
160 160 except KeyError, e:
161 161 raise AttributeError(e)
162 162
163 163 def __delattr__(self, key):
164 164 try:
165 165 dict.__delitem__(self, key)
166 166 except KeyError, e:
167 167 raise AttributeError(e)
168 168
169 169
170 170 #-----------------------------------------------------------------------------
171 171 # Config loading classes
172 172 #-----------------------------------------------------------------------------
173 173
174 174
175 175 class ConfigLoader(object):
176 176 """A object for loading configurations from just about anywhere.
177 177
178 178 The resulting configuration is packaged as a :class:`Struct`.
179 179
180 180 Notes
181 181 -----
182 182 A :class:`ConfigLoader` does one thing: load a config from a source
183 183 (file, command line arguments) and returns the data as a :class:`Struct`.
184 184 There are lots of things that :class:`ConfigLoader` does not do. It does
185 185 not implement complex logic for finding config files. It does not handle
186 186 default values or merge multiple configs. These things need to be
187 187 handled elsewhere.
188 188 """
189 189
190 190 def __init__(self):
191 191 """A base class for config loaders.
192 192
193 193 Examples
194 194 --------
195 195
196 196 >>> cl = ConfigLoader()
197 197 >>> config = cl.load_config()
198 198 >>> config
199 199 {}
200 200 """
201 201 self.clear()
202 202
203 203 def clear(self):
204 204 self.config = Config()
205 205
206 206 def load_config(self):
207 207 """Load a config from somewhere, return a :class:`Config` instance.
208 208
209 209 Usually, this will cause self.config to be set and then returned.
210 210 However, in most cases, :meth:`ConfigLoader.clear` should be called
211 211 to erase any previous state.
212 212 """
213 213 self.clear()
214 214 return self.config
215 215
216 216
217 217 class FileConfigLoader(ConfigLoader):
218 218 """A base class for file based configurations.
219 219
220 220 As we add more file based config loaders, the common logic should go
221 221 here.
222 222 """
223 223 pass
224 224
225 225
226 226 class PyFileConfigLoader(FileConfigLoader):
227 227 """A config loader for pure python files.
228 228
229 229 This calls execfile on a plain python file and looks for attributes
230 230 that are all caps. These attribute are added to the config Struct.
231 231 """
232 232
233 233 def __init__(self, filename, path=None):
234 234 """Build a config loader for a filename and path.
235 235
236 236 Parameters
237 237 ----------
238 238 filename : str
239 239 The file name of the config file.
240 240 path : str, list, tuple
241 241 The path to search for the config file on, or a sequence of
242 242 paths to try in order.
243 243 """
244 244 super(PyFileConfigLoader, self).__init__()
245 245 self.filename = filename
246 246 self.path = path
247 247 self.full_filename = ''
248 248 self.data = None
249 249
250 250 def load_config(self):
251 251 """Load the config from a file and return it as a Struct."""
252 252 self.clear()
253 253 self._find_file()
254 254 self._read_file_as_dict()
255 255 self._convert_to_config()
256 256 return self.config
257 257
258 258 def _find_file(self):
259 259 """Try to find the file by searching the paths."""
260 260 self.full_filename = filefind(self.filename, self.path)
261 261
262 262 def _read_file_as_dict(self):
263 263 """Load the config file into self.config, with recursive loading."""
264 264 # This closure is made available in the namespace that is used
265 265 # to exec the config file. This allows users to call
266 266 # load_subconfig('myconfig.py') to load config files recursively.
267 267 # It needs to be a closure because it has references to self.path
268 268 # and self.config. The sub-config is loaded with the same path
269 269 # as the parent, but it uses an empty config which is then merged
270 270 # with the parents.
271 271 def load_subconfig(fname):
272 272 loader = PyFileConfigLoader(fname, self.path)
273 273 try:
274 274 sub_config = loader.load_config()
275 275 except IOError:
276 276 # Pass silently if the sub config is not there. This happens
277 277 # when a user us using a profile, but not the default config.
278 278 pass
279 279 else:
280 280 self.config._merge(sub_config)
281 281
282 282 # Again, this needs to be a closure and should be used in config
283 283 # files to get the config being loaded.
284 284 def get_config():
285 285 return self.config
286 286
287 287 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
288 execfile(self.full_filename, namespace)
288 conf_filename = self.full_filename.encode(sys.getfilesystemencoding())
289 execfile(conf_filename, namespace)
289 290
290 291 def _convert_to_config(self):
291 292 if self.data is None:
292 293 ConfigLoaderError('self.data does not exist')
293 294
294 295
295 296 class CommandLineConfigLoader(ConfigLoader):
296 297 """A config loader for command line arguments.
297 298
298 299 As we add more command line based loaders, the common logic should go
299 300 here.
300 301 """
301 302
302 303
303 304 class ArgParseConfigLoader(CommandLineConfigLoader):
304 305
305 306 def __init__(self, argv=None, *parser_args, **parser_kw):
306 307 """Create a config loader for use with argparse.
307 308
308 309 Parameters
309 310 ----------
310 311
311 312 argv : optional, list
312 313 If given, used to read command-line arguments from, otherwise
313 314 sys.argv[1:] is used.
314 315
315 316 parser_args : tuple
316 317 A tuple of positional arguments that will be passed to the
317 318 constructor of :class:`argparse.ArgumentParser`.
318 319
319 320 parser_kw : dict
320 321 A tuple of keyword arguments that will be passed to the
321 322 constructor of :class:`argparse.ArgumentParser`.
322 323 """
323 324 super(CommandLineConfigLoader, self).__init__()
324 325 if argv == None:
325 326 argv = sys.argv[1:]
326 327 self.argv = argv
327 328 self.parser_args = parser_args
328 329 self.version = parser_kw.pop("version", None)
329 330 kwargs = dict(argument_default=argparse.SUPPRESS)
330 331 kwargs.update(parser_kw)
331 332 self.parser_kw = kwargs
332 333
333 334 def load_config(self, args=None):
334 335 """Parse command line arguments and return as a Struct.
335 336
336 337 Parameters
337 338 ----------
338 339
339 340 args : optional, list
340 341 If given, a list with the structure of sys.argv[1:] to parse
341 342 arguments from. If not given, the instance's self.argv attribute
342 343 (given at construction time) is used."""
343 344 self.clear()
344 345 if args is None:
345 346 args = self.argv
346 347 self._create_parser()
347 348 self._parse_args(args)
348 349 self._convert_to_config()
349 350 return self.config
350 351
351 352 def get_extra_args(self):
352 353 if hasattr(self, 'extra_args'):
353 354 return self.extra_args
354 355 else:
355 356 return []
356 357
357 358 def _create_parser(self):
358 359 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
359 360 self._add_arguments()
360 361
361 362 def _add_arguments(self):
362 363 raise NotImplementedError("subclasses must implement _add_arguments")
363 364
364 365 def _parse_args(self, args):
365 366 """self.parser->self.parsed_data"""
366 367 self.parsed_data, self.extra_args = self.parser.parse_known_args(args)
367 368
368 369 def _convert_to_config(self):
369 370 """self.parsed_data->self.config"""
370 371 for k, v in vars(self.parsed_data).iteritems():
371 372 exec_str = 'self.config.' + k + '= v'
372 373 exec exec_str in locals(), globals()
373 374
374 375
@@ -1,33 +1,67 b''
1 1 """Tests for IPython.core.application"""
2 2
3 3 import os
4 4 import tempfile
5 5
6 6 from IPython.core.application import Application
7 7
8 8 def test_unicode_cwd():
9 """Check that IPython can start with unicode characters in the path."""
9 """Check that IPython starts with non-ascii characters in the path."""
10 10 wd = tempfile.mkdtemp(suffix="€")
11 11
12 12 old_wd = os.getcwdu()
13 13 os.chdir(wd)
14 14 #raise Exception(repr(os.getcwd()))
15 15 try:
16 16 app = Application()
17 17 # The lines below are copied from Application.initialize()
18 18 app.create_default_config()
19 19 app.log_default_config()
20 20 app.set_default_config_log_level()
21 21
22 22 # Find resources needed for filesystem access, using information from
23 23 # the above two
24 24 app.find_ipython_dir()
25 25 app.find_resources()
26 26 app.find_config_file_name()
27 27 app.find_config_file_paths()
28 28
29 29 # File-based config
30 30 app.pre_load_file_config()
31 31 app.load_file_config(suppress_errors=False)
32 32 finally:
33 33 os.chdir(old_wd)
34
35 def test_unicode_ipdir():
36 """Check that IPython starts with non-ascii characters in the IP dir."""
37 ipdir = tempfile.mkdtemp(suffix="€")
38
39 # Create the config file, so it tries to load it.
40 with open(os.path.join(ipdir, 'ipython_config.py'), "w") as f:
41 pass
42
43 old_ipdir1 = os.environ.pop("IPYTHONDIR", None)
44 old_ipdir2 = os.environ.pop("IPYTHON_DIR", None)
45 os.environ["IPYTHONDIR"] = ipdir
46 try:
47 app = Application()
48 # The lines below are copied from Application.initialize()
49 app.create_default_config()
50 app.log_default_config()
51 app.set_default_config_log_level()
52
53 # Find resources needed for filesystem access, using information from
54 # the above two
55 app.find_ipython_dir()
56 app.find_resources()
57 app.find_config_file_name()
58 app.find_config_file_paths()
59
60 # File-based config
61 app.pre_load_file_config()
62 app.load_file_config(suppress_errors=False)
63 finally:
64 if old_ipdir1:
65 os.environ["IPYTHONDIR"] = old_ipdir1
66 if old_ipdir2:
67 os.environ["IPYTHONDIR"] = old_ipdir2
General Comments 0
You need to be logged in to leave comments. Login now