Skip to content

Commit 0113466

Browse files
committed
Re-worked the presets error hierarchy to have more descriptive and information packed exception classes. Also re-worked the PresetParser to catch duplicate preset declarations and duplicate options in a single preset. There's some special shim-code to handle the disconnect between the Python 2 ConfigParser module and the Python 3 update which adds DuplicateOptionError.
1 parent 009b6aa commit 0113466

File tree

6 files changed

+255
-107
lines changed

6 files changed

+255
-107
lines changed

utils/build-script

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -988,9 +988,8 @@ def main_preset():
988988

989989
try:
990990
preset_parser.read(args.preset_file_names)
991-
except presets.UnparsedFilesError as e:
992-
diagnostics.fatal('unable to parse preset files {}'
993-
.format(e.message))
991+
except presets.Error as e:
992+
diagnostics.fatal(e.message)
994993

995994
if args.show_presets:
996995
for name in sorted(preset_parser.preset_names(),
@@ -1009,14 +1008,8 @@ def main_preset():
10091008
try:
10101009
preset = preset_parser.get_preset(args.preset,
10111010
vars=args.preset_substitutions)
1012-
except presets.InterpolationError as e:
1013-
diagnostics.fatal('missing value for substitution "{}" in preset "{}"'
1014-
.format(e.message, args.preset))
1015-
except presets.MissingOptionError as e:
1016-
diagnostics.fatal('missing option(s) for preset "{}": {}'
1017-
.format(args.preset, e.message))
1018-
except presets.PresetNotFoundError:
1019-
diagnostics.fatal('preset "{}" not found'.format(args.preset))
1011+
except presets.Error as e:
1012+
diagnostics.fatal(e.message)
10201013

10211014
preset_args = migrate_swift_sdks(preset.format_args())
10221015

utils/build_swift/migration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Temporary module with functionaly used to migrate away from build-script-impl.
1212
"""
1313

14+
from __future__ import absolute_import, unicode_literals
1415

1516
from swift_build_support.swift_build_support.targets import \
1617
StdlibDeploymentTarget

utils/build_swift/presets.py

Lines changed: 136 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,37 @@
1111
Swift preset parsing and handling functionality.
1212
"""
1313

14+
from __future__ import absolute_import, unicode_literals
1415

1516
from collections import namedtuple
17+
from contextlib import contextmanager
1618

1719
try:
18-
# Python 3
19-
import configparser
20-
from io import StringIO
21-
except ImportError:
20+
# Python 2
2221
import ConfigParser as configparser
2322
from StringIO import StringIO
23+
except ImportError:
24+
import configparser
25+
from io import StringIO
2426

2527

2628
__all__ = [
27-
'Preset',
28-
'PresetParser',
29-
29+
'Error',
30+
'DuplicatePresetError',
31+
'DuplicateOptionError',
3032
'InterpolationError',
31-
'MissingOptionError',
3233
'PresetNotFoundError',
3334
'UnparsedFilesError',
35+
36+
'Preset',
37+
'PresetParser',
3438
]
3539

3640

3741
# -----------------------------------------------------------------------------
3842

43+
_PRESET_PREFIX = 'preset: '
44+
3945
_Mixin = namedtuple('_Mixin', ['name'])
4046
_Argument = namedtuple('_Argument', ['name', 'value'])
4147
_RawPreset = namedtuple('_RawPreset', ['name', 'options'])
@@ -45,10 +51,7 @@ def _interpolate_string(string, values):
4551
if string is None:
4652
return string
4753

48-
try:
49-
return string % values
50-
except KeyError as e:
51-
raise InterpolationError(e.message)
54+
return string % values
5255

5356

5457
def _remove_prefix(string, prefix):
@@ -57,37 +60,119 @@ def _remove_prefix(string, prefix):
5760
return string
5861

5962

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+
6099
# -----------------------------------------------------------------------------
100+
# Error classes
61101

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.
65104
"""
66105

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
68113

114+
__repr__ = __str__
69115

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.
72119
"""
73120

74-
pass
121+
def __init__(self, preset_name):
122+
Error.__init__(self, '{} already exists'.format(preset_name))
123+
124+
self.preset_name = preset_name
75125

76126

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.
79129
"""
80130

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
82137

83138

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.
86142
"""
87143

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
89162

90163

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+
91176
class Preset(namedtuple('Preset', ['name', 'args'])):
92177
"""Container class used to wrap preset names and expanded argument lists.
93178
"""
@@ -109,47 +194,44 @@ def format_args(self):
109194
return args
110195

111196

112-
# -----------------------------------------------------------------------------
113-
114197
class PresetParser(object):
115198
"""Parser class used to read and manipulate Swift preset files.
116199
"""
117200

118-
_PRESET_PREFIX = 'preset: '
119-
120201
def __init__(self):
121202
self._parser = configparser.RawConfigParser(allow_no_value=True)
122203
self._presets = {}
123204

124205
def _parse_raw_preset(self, section):
125-
preset_name = _remove_prefix(section, PresetParser._PRESET_PREFIX)
206+
preset_name = _remove_prefix(section, _PRESET_PREFIX)
126207

127208
try:
128209
section_items = self._parser.items(section)
129210
except configparser.InterpolationMissingOptionError as e:
130-
raise MissingOptionError(e)
211+
raise InterpolationError(preset_name, e.option, e.rawval,
212+
e.reference)
131213

132214
args = []
133-
for (name, value) in section_items:
215+
for (option, value) in section_items:
134216
# Ignore the '--' separator, it's no longer necessary
135-
if name == 'dash-dash':
217+
if option == 'dash-dash':
136218
continue
137219

138-
# Parse out mixin names
139-
if name == 'mixin-preset':
220+
# Parse out mixin options
221+
if option == 'mixin-preset':
140222
lines = value.strip().splitlines()
141-
args += [_Mixin(name.strip()) for name in lines]
223+
args += [_Mixin(option.strip()) for option in lines]
142224
continue
143225

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))
146228

147229
return _RawPreset(preset_name, args)
148230

149231
def _parse_raw_presets(self):
150232
for section in self._parser.sections():
151233
# Skip all non-preset sections
152-
if not section.startswith(PresetParser._PRESET_PREFIX):
234+
if not section.startswith(_PRESET_PREFIX):
153235
continue
154236

155237
raw_preset = self._parse_raw_preset(section)
@@ -160,7 +242,8 @@ def read(self, filenames):
160242
of the files couldn't be read.
161243
"""
162244

163-
parsed_files = self._parser.read(filenames)
245+
with _convert_configparser_errors():
246+
parsed_files = self._parser.read(filenames)
164247

165248
unparsed_files = set(filenames) - set(parsed_files)
166249
if len(unparsed_files) > 0:
@@ -178,13 +261,14 @@ def read_string(self, string):
178261
"""Reads and parses a string containing preset definintions.
179262
"""
180263

181-
fp = StringIO(unicode(string))
264+
fp = StringIO(string)
182265

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)
188272

189273
self._parse_raw_presets()
190274

@@ -215,14 +299,19 @@ def _resolve_preset_mixins(self, raw_preset):
215299
elif isinstance(option, _Argument):
216300
args.append((option.name, option.value))
217301
else:
302+
# Should be unreachable
218303
raise ValueError('invalid argument type: {}', option.__class__)
219304

220305
return Preset(raw_preset.name, args)
221306

222307
def _interpolate_preset_vars(self, preset, vars):
223308
interpolated_args = []
224309
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+
226315
interpolated_args.append((name, value))
227316

228317
return Preset(preset.name, interpolated_args)

utils/build_swift/tests/test_migration.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88

99

10-
from .utils import TestCase
10+
from __future__ import absolute_import, unicode_literals
11+
12+
from .utils import TestCase, add_metaclass
1113
from .. import migration
1214

1315

@@ -52,10 +54,9 @@ def test(self):
5254
return test
5355

5456

57+
@add_metaclass(TestMigrateSwiftSDKsMeta)
5558
class TestMigrateSwiftSDKs(TestCase):
5659

57-
__metaclass__ = TestMigrateSwiftSDKsMeta
58-
5960
def test_multiple_swift_sdk_flags(self):
6061
args = [
6162
'--swift-sdks=OSX',

0 commit comments

Comments
 (0)