mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
* Implemented a presets module which includes a more robust and easy to understand parser. Moved the swift-sdks migration code to a new migration module and added testing for both the presets and migration modules. Also converted build-script to use the new presets parser. * Switched the expansion algorithm to resolve mixins in-place (little known feature) and also changed the parser to skip all non-preset sections. Tests are included for these two behaviors. * 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.
345 lines
9.6 KiB
Python
345 lines
9.6 KiB
Python
# This source file is part of the Swift.org open source project
|
|
#
|
|
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
|
|
# Licensed under Apache License v2.0 with Runtime Library Exception
|
|
#
|
|
# See https://swift.org/LICENSE.txt for license information
|
|
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
|
|
|
|
"""
|
|
Swift preset parsing and handling functionality.
|
|
"""
|
|
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
from collections import namedtuple
|
|
from contextlib import contextmanager
|
|
|
|
try:
|
|
# Python 2
|
|
import ConfigParser as configparser
|
|
from StringIO import StringIO
|
|
except ImportError:
|
|
import configparser
|
|
from io import StringIO
|
|
|
|
|
|
__all__ = [
|
|
'Error',
|
|
'DuplicatePresetError',
|
|
'DuplicateOptionError',
|
|
'InterpolationError',
|
|
'PresetNotFoundError',
|
|
'UnparsedFilesError',
|
|
|
|
'Preset',
|
|
'PresetParser',
|
|
]
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
_PRESET_PREFIX = 'preset: '
|
|
|
|
_Mixin = namedtuple('_Mixin', ['name'])
|
|
_Argument = namedtuple('_Argument', ['name', 'value'])
|
|
_RawPreset = namedtuple('_RawPreset', ['name', 'options'])
|
|
|
|
|
|
def _interpolate_string(string, values):
|
|
if string is None:
|
|
return string
|
|
|
|
return string % values
|
|
|
|
|
|
def _remove_prefix(string, prefix):
|
|
if string.startswith(prefix):
|
|
return string[len(prefix):]
|
|
return string
|
|
|
|
|
|
@contextmanager
|
|
def _catch_duplicate_option_error():
|
|
"""Shim context object used for catching and rethrowing configparser's
|
|
DuplicateOptionError, which was added in the Python 3 refactor.
|
|
"""
|
|
|
|
if hasattr(configparser, 'DuplicateOptionError'):
|
|
try:
|
|
yield
|
|
except configparser.DuplicateOptionError as e:
|
|
preset_name = _remove_prefix(e.section, _PRESET_PREFIX)
|
|
raise DuplicateOptionError(preset_name, e.option)
|
|
|
|
else:
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def _catch_duplicate_section_error():
|
|
"""Shim context object used for catching and rethrowing configparser's
|
|
DuplicateSectionError.
|
|
"""
|
|
|
|
try:
|
|
yield
|
|
except configparser.DuplicateSectionError as e:
|
|
preset_name = _remove_prefix(e.section, _PRESET_PREFIX)
|
|
raise DuplicatePresetError(preset_name)
|
|
|
|
|
|
@contextmanager
|
|
def _convert_configparser_errors():
|
|
with _catch_duplicate_option_error(), _catch_duplicate_section_error():
|
|
yield
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Error classes
|
|
|
|
class Error(Exception):
|
|
"""Base class for preset errors.
|
|
"""
|
|
|
|
def __init__(self, message=''):
|
|
super(Error, self).__init__(self, message)
|
|
|
|
self.message = message
|
|
|
|
def __str__(self):
|
|
return self.message
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class DuplicatePresetError(Error):
|
|
"""Raised when an existing preset would be overriden.
|
|
"""
|
|
|
|
def __init__(self, preset_name):
|
|
Error.__init__(self, '{} already exists'.format(preset_name))
|
|
|
|
self.preset_name = preset_name
|
|
|
|
|
|
class DuplicateOptionError(Error):
|
|
"""Raised when an option is repeated in a single preset.
|
|
"""
|
|
|
|
def __init__(self, preset_name, option):
|
|
Error.__init__(self, '{} already exists in preset {}'.format(
|
|
option, preset_name))
|
|
|
|
self.preset_name = preset_name
|
|
self.option = option
|
|
|
|
|
|
class InterpolationError(Error):
|
|
"""Raised when an error is encountered while interpolating use-provided
|
|
values in preset arguments.
|
|
"""
|
|
|
|
def __init__(self, preset_name, option, rawval, reference):
|
|
Error.__init__(self, 'no value found for {} in "{}"'.format(
|
|
reference, rawval))
|
|
|
|
self.preset_name = preset_name
|
|
self.option = option
|
|
self.rawval = rawval
|
|
self.reference = reference
|
|
|
|
|
|
class PresetNotFoundError(Error):
|
|
"""Raised when a requested preset cannot be found.
|
|
"""
|
|
|
|
def __init__(self, preset_name):
|
|
Error.__init__(self, '{} not found'.format(preset_name))
|
|
|
|
self.preset_name = preset_name
|
|
|
|
|
|
class UnparsedFilesError(Error):
|
|
"""Raised when an error was encountered parsing one or more preset files.
|
|
"""
|
|
|
|
def __init__(self, filenames):
|
|
Error.__init__(self, 'unable to parse files: {}'.format(filenames))
|
|
|
|
self.filenames = filenames
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
class Preset(namedtuple('Preset', ['name', 'args'])):
|
|
"""Container class used to wrap preset names and expanded argument lists.
|
|
"""
|
|
|
|
# Keeps memory costs low according to the docs
|
|
__slots__ = ()
|
|
|
|
def format_args(self):
|
|
"""Format argument pairs for use in the command line.
|
|
"""
|
|
|
|
args = []
|
|
for (name, value) in self.args:
|
|
if value is None:
|
|
args.append(name)
|
|
else:
|
|
args.append('{}={}'.format(name, value))
|
|
|
|
return args
|
|
|
|
|
|
class PresetParser(object):
|
|
"""Parser class used to read and manipulate Swift preset files.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._parser = configparser.RawConfigParser(allow_no_value=True)
|
|
self._presets = {}
|
|
|
|
def _parse_raw_preset(self, section):
|
|
preset_name = _remove_prefix(section, _PRESET_PREFIX)
|
|
|
|
try:
|
|
section_items = self._parser.items(section)
|
|
except configparser.InterpolationMissingOptionError as e:
|
|
raise InterpolationError(preset_name, e.option, e.rawval,
|
|
e.reference)
|
|
|
|
args = []
|
|
for (option, value) in section_items:
|
|
# Ignore the '--' separator, it's no longer necessary
|
|
if option == 'dash-dash':
|
|
continue
|
|
|
|
# Parse out mixin options
|
|
if option == 'mixin-preset':
|
|
lines = value.strip().splitlines()
|
|
args += [_Mixin(option.strip()) for option in lines]
|
|
continue
|
|
|
|
option = '--' + option # Format as a command-line option
|
|
args.append(_Argument(option, value))
|
|
|
|
return _RawPreset(preset_name, args)
|
|
|
|
def _parse_raw_presets(self):
|
|
for section in self._parser.sections():
|
|
# Skip all non-preset sections
|
|
if not section.startswith(_PRESET_PREFIX):
|
|
continue
|
|
|
|
raw_preset = self._parse_raw_preset(section)
|
|
self._presets[raw_preset.name] = raw_preset
|
|
|
|
def read(self, filenames):
|
|
"""Reads and parses preset files. Throws an UnparsedFilesError if any
|
|
of the files couldn't be read.
|
|
"""
|
|
|
|
with _convert_configparser_errors():
|
|
parsed_files = self._parser.read(filenames)
|
|
|
|
unparsed_files = set(filenames) - set(parsed_files)
|
|
if len(unparsed_files) > 0:
|
|
raise UnparsedFilesError(list(unparsed_files))
|
|
|
|
self._parse_raw_presets()
|
|
|
|
def read_file(self, file):
|
|
"""Reads and parses a single file.
|
|
"""
|
|
|
|
self.read([file])
|
|
|
|
def read_string(self, string):
|
|
"""Reads and parses a string containing preset definintions.
|
|
"""
|
|
|
|
fp = StringIO(string)
|
|
|
|
with _convert_configparser_errors():
|
|
# ConfigParser changes drastically from Python 2 to 3
|
|
if hasattr(self._parser, 'read_file'):
|
|
self._parser.read_file(fp)
|
|
else:
|
|
self._parser.readfp(fp)
|
|
|
|
self._parse_raw_presets()
|
|
|
|
def _get_preset(self, name):
|
|
preset = self._presets.get(name)
|
|
if preset is None:
|
|
raise PresetNotFoundError(name)
|
|
|
|
if isinstance(preset, _RawPreset):
|
|
preset = self._resolve_preset_mixins(preset)
|
|
|
|
# Cache resolved preset
|
|
self._presets[name] = preset
|
|
|
|
return preset
|
|
|
|
def _resolve_preset_mixins(self, raw_preset):
|
|
"""Resolve all mixins in a preset, fully expanding the arguments list.
|
|
"""
|
|
|
|
assert isinstance(raw_preset, _RawPreset)
|
|
|
|
# Expand mixin arguments
|
|
args = []
|
|
for option in raw_preset.options:
|
|
if isinstance(option, _Mixin):
|
|
args += self._get_preset(option.name).args
|
|
elif isinstance(option, _Argument):
|
|
args.append((option.name, option.value))
|
|
else:
|
|
# Should be unreachable
|
|
raise ValueError('invalid argument type: {}', option.__class__)
|
|
|
|
return Preset(raw_preset.name, args)
|
|
|
|
def _interpolate_preset_vars(self, preset, vars):
|
|
interpolated_args = []
|
|
for (name, value) in preset.args:
|
|
try:
|
|
value = _interpolate_string(value, vars)
|
|
except KeyError as e:
|
|
raise InterpolationError(preset.name, name, value, e.args[0])
|
|
|
|
interpolated_args.append((name, value))
|
|
|
|
return Preset(preset.name, interpolated_args)
|
|
|
|
def get_preset(self, name, raw=False, vars=None):
|
|
"""Returns the preset with the requested name or throws a
|
|
PresetNotFoundError.
|
|
|
|
If raw is False vars will be interpolated into the preset arguments.
|
|
Otherwise presets will be returned without interpolation.
|
|
|
|
Presets are retrieved using a dynamic caching algorithm that expands
|
|
only the requested preset and it's mixins recursively. Every expanded
|
|
preset is then cached. All subsequent expansions or calls to
|
|
`get_preset` for any pre-expanded presets will use the cached results.
|
|
"""
|
|
|
|
vars = vars or {}
|
|
|
|
preset = self._get_preset(name)
|
|
if not raw:
|
|
preset = self._interpolate_preset_vars(preset, vars)
|
|
|
|
return preset
|
|
|
|
def preset_names(self):
|
|
"""Returns a list of all parsed preset names.
|
|
"""
|
|
|
|
return self._presets.keys()
|