| # 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() |