11
11
Swift preset parsing and handling functionality.
12
12
"""
13
13
14
+ from __future__ import absolute_import , unicode_literals
14
15
15
16
from collections import namedtuple
17
+ from contextlib import contextmanager
16
18
17
19
try :
18
- # Python 3
19
- import configparser
20
- from io import StringIO
21
- except ImportError :
20
+ # Python 2
22
21
import ConfigParser as configparser
23
22
from StringIO import StringIO
23
+ except ImportError :
24
+ import configparser
25
+ from io import StringIO
24
26
25
27
26
28
__all__ = [
27
- 'Preset ' ,
28
- 'PresetParser ' ,
29
-
29
+ 'Error ' ,
30
+ 'DuplicatePresetError ' ,
31
+ 'DuplicateOptionError' ,
30
32
'InterpolationError' ,
31
- 'MissingOptionError' ,
32
33
'PresetNotFoundError' ,
33
34
'UnparsedFilesError' ,
35
+
36
+ 'Preset' ,
37
+ 'PresetParser' ,
34
38
]
35
39
36
40
37
41
# -----------------------------------------------------------------------------
38
42
43
+ _PRESET_PREFIX = 'preset: '
44
+
39
45
_Mixin = namedtuple ('_Mixin' , ['name' ])
40
46
_Argument = namedtuple ('_Argument' , ['name' , 'value' ])
41
47
_RawPreset = namedtuple ('_RawPreset' , ['name' , 'options' ])
@@ -45,10 +51,7 @@ def _interpolate_string(string, values):
45
51
if string is None :
46
52
return string
47
53
48
- try :
49
- return string % values
50
- except KeyError as e :
51
- raise InterpolationError (e .message )
54
+ return string % values
52
55
53
56
54
57
def _remove_prefix (string , prefix ):
@@ -57,37 +60,119 @@ def _remove_prefix(string, prefix):
57
60
return string
58
61
59
62
63
+ @contextmanager
64
+ def _catch_duplicate_option_error ():
65
+ """Shim context object used for catching and rethrowing configparser's
66
+ DuplicateOptionError, which was added in the Python 3 refactor.
67
+ """
68
+
69
+ if hasattr (configparser , 'DuplicateOptionError' ):
70
+ try :
71
+ yield
72
+ except configparser .DuplicateOptionError as e :
73
+ preset_name = _remove_prefix (e .section , _PRESET_PREFIX )
74
+ raise DuplicateOptionError (preset_name , e .option )
75
+
76
+ else :
77
+ yield
78
+
79
+
80
+ @contextmanager
81
+ def _catch_duplicate_section_error ():
82
+ """Shim context object used for catching and rethrowing configparser's
83
+ DuplicateSectionError.
84
+ """
85
+
86
+ try :
87
+ yield
88
+ except configparser .DuplicateSectionError as e :
89
+ preset_name = _remove_prefix (e .section , _PRESET_PREFIX )
90
+ raise DuplicatePresetError (preset_name )
91
+
92
+
93
+ @contextmanager
94
+ def _convert_configparser_errors ():
95
+ with _catch_duplicate_option_error (), _catch_duplicate_section_error ():
96
+ yield
97
+
98
+
60
99
# -----------------------------------------------------------------------------
100
+ # Error classes
61
101
62
- class InterpolationError (Exception ):
63
- """Error indicating a filaure while interpolating variables in preset
64
- argument values.
102
+ class Error (Exception ):
103
+ """Base class for preset errors.
65
104
"""
66
105
67
- pass
106
+ def __init__ (self , message = '' ):
107
+ super (Error , self ).__init__ (self , message )
108
+
109
+ self .message = message
110
+
111
+ def __str__ (self ):
112
+ return self .message
68
113
114
+ __repr__ = __str__
69
115
70
- class MissingOptionError (Exception ):
71
- """Error indicating a missing option while parsing presets.
116
+
117
+ class DuplicatePresetError (Error ):
118
+ """Raised when an existing preset would be overriden.
72
119
"""
73
120
74
- pass
121
+ def __init__ (self , preset_name ):
122
+ Error .__init__ (self , '{} already exists' .format (preset_name ))
123
+
124
+ self .preset_name = preset_name
75
125
76
126
77
- class PresetNotFoundError ( Exception ):
78
- """Error indicating failure when attempting to get a preset.
127
+ class DuplicateOptionError ( Error ):
128
+ """Raised when an option is repeated in a single preset.
79
129
"""
80
130
81
- pass
131
+ def __init__ (self , preset_name , option ):
132
+ Error .__init__ (self , '{} already exists in preset {}' .format (
133
+ option , preset_name ))
134
+
135
+ self .preset_name = preset_name
136
+ self .option = option
82
137
83
138
84
- class UnparsedFilesError (Exception ):
85
- """Error indicating failure when parsing preset files.
139
+ class InterpolationError (Error ):
140
+ """Raised when an error is encountered while interpolating use-provided
141
+ values in preset arguments.
86
142
"""
87
143
88
- pass
144
+ def __init__ (self , preset_name , option , rawval , reference ):
145
+ Error .__init__ (self , 'no value found for {} in "{}"' .format (
146
+ reference , rawval ))
147
+
148
+ self .preset_name = preset_name
149
+ self .option = option
150
+ self .rawval = rawval
151
+ self .reference = reference
152
+
153
+
154
+ class PresetNotFoundError (Error ):
155
+ """Raised when a requested preset cannot be found.
156
+ """
157
+
158
+ def __init__ (self , preset_name ):
159
+ Error .__init__ (self , '{} not found' .format (preset_name ))
160
+
161
+ self .preset_name = preset_name
89
162
90
163
164
+ class UnparsedFilesError (Error ):
165
+ """Raised when an error was encountered parsing one or more preset files.
166
+ """
167
+
168
+ def __init__ (self , filenames ):
169
+ Error .__init__ (self , 'unable to parse files: {}' .format (filenames ))
170
+
171
+ self .filenames = filenames
172
+
173
+
174
+ # -----------------------------------------------------------------------------
175
+
91
176
class Preset (namedtuple ('Preset' , ['name' , 'args' ])):
92
177
"""Container class used to wrap preset names and expanded argument lists.
93
178
"""
@@ -109,47 +194,44 @@ def format_args(self):
109
194
return args
110
195
111
196
112
- # -----------------------------------------------------------------------------
113
-
114
197
class PresetParser (object ):
115
198
"""Parser class used to read and manipulate Swift preset files.
116
199
"""
117
200
118
- _PRESET_PREFIX = 'preset: '
119
-
120
201
def __init__ (self ):
121
202
self ._parser = configparser .RawConfigParser (allow_no_value = True )
122
203
self ._presets = {}
123
204
124
205
def _parse_raw_preset (self , section ):
125
- preset_name = _remove_prefix (section , PresetParser . _PRESET_PREFIX )
206
+ preset_name = _remove_prefix (section , _PRESET_PREFIX )
126
207
127
208
try :
128
209
section_items = self ._parser .items (section )
129
210
except configparser .InterpolationMissingOptionError as e :
130
- raise MissingOptionError (e )
211
+ raise InterpolationError (preset_name , e .option , e .rawval ,
212
+ e .reference )
131
213
132
214
args = []
133
- for (name , value ) in section_items :
215
+ for (option , value ) in section_items :
134
216
# Ignore the '--' separator, it's no longer necessary
135
- if name == 'dash-dash' :
217
+ if option == 'dash-dash' :
136
218
continue
137
219
138
- # Parse out mixin names
139
- if name == 'mixin-preset' :
220
+ # Parse out mixin options
221
+ if option == 'mixin-preset' :
140
222
lines = value .strip ().splitlines ()
141
- args += [_Mixin (name .strip ()) for name in lines ]
223
+ args += [_Mixin (option .strip ()) for option in lines ]
142
224
continue
143
225
144
- name = '--' + name # Format as an option name
145
- args .append (_Argument (name , value ))
226
+ option = '--' + option # Format as a command-line option
227
+ args .append (_Argument (option , value ))
146
228
147
229
return _RawPreset (preset_name , args )
148
230
149
231
def _parse_raw_presets (self ):
150
232
for section in self ._parser .sections ():
151
233
# Skip all non-preset sections
152
- if not section .startswith (PresetParser . _PRESET_PREFIX ):
234
+ if not section .startswith (_PRESET_PREFIX ):
153
235
continue
154
236
155
237
raw_preset = self ._parse_raw_preset (section )
@@ -160,7 +242,8 @@ def read(self, filenames):
160
242
of the files couldn't be read.
161
243
"""
162
244
163
- parsed_files = self ._parser .read (filenames )
245
+ with _convert_configparser_errors ():
246
+ parsed_files = self ._parser .read (filenames )
164
247
165
248
unparsed_files = set (filenames ) - set (parsed_files )
166
249
if len (unparsed_files ) > 0 :
@@ -178,13 +261,14 @@ def read_string(self, string):
178
261
"""Reads and parses a string containing preset definintions.
179
262
"""
180
263
181
- fp = StringIO (unicode ( string ) )
264
+ fp = StringIO (string )
182
265
183
- # ConfigParser changes drastically from Python 2 to 3
184
- if hasattr (self ._parser , 'read_file' ):
185
- self ._parser .read_file (fp )
186
- else :
187
- self ._parser .readfp (fp )
266
+ with _convert_configparser_errors ():
267
+ # ConfigParser changes drastically from Python 2 to 3
268
+ if hasattr (self ._parser , 'read_file' ):
269
+ self ._parser .read_file (fp )
270
+ else :
271
+ self ._parser .readfp (fp )
188
272
189
273
self ._parse_raw_presets ()
190
274
@@ -215,14 +299,19 @@ def _resolve_preset_mixins(self, raw_preset):
215
299
elif isinstance (option , _Argument ):
216
300
args .append ((option .name , option .value ))
217
301
else :
302
+ # Should be unreachable
218
303
raise ValueError ('invalid argument type: {}' , option .__class__ )
219
304
220
305
return Preset (raw_preset .name , args )
221
306
222
307
def _interpolate_preset_vars (self , preset , vars ):
223
308
interpolated_args = []
224
309
for (name , value ) in preset .args :
225
- value = _interpolate_string (value , vars )
310
+ try :
311
+ value = _interpolate_string (value , vars )
312
+ except KeyError as e :
313
+ raise InterpolationError (preset .name , name , value , e .args [0 ])
314
+
226
315
interpolated_args .append ((name , value ))
227
316
228
317
return Preset (preset .name , interpolated_args )
0 commit comments