Source code for matrix

# -*- coding: utf-8 -*-
import re
import warnings
from fnmatch import fnmatch
from itertools import product

from backports.configparser2 import ConfigParser

try:
    from collections import OrderedDict
except ImportError:
    from .ordereddict import OrderedDict

try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

__version__ = "2.0.1"

entry_rx = re.compile(r"""
    ^
    ((?P<merge>\?))?
    ((?P<alias>[^\?:]*):)?
    \s*(?P<value>[^!&]+?)\s*
    (?P<reducers>[!&].+)?
    $
""", re.VERBOSE)
reducer_rx = re.compile(r"""
    \s*
    (?P<type>[!&])
    (?P<variable>[^!&\[\]]+)
    \[(?P<glob>[^\[\]]+)\]
    \s*
""", re.VERBOSE)

special_chars_rx = re.compile(r'[\\/:>?|\[\]< ]+')


class ParseError(Exception):
    pass


class DuplicateEntry(UserWarning):
    def __str__(self):
        return "Duplicate entry %r (from %r). Conflicts with %r - it has the same alias." % self.args

    __repr__ = __str__


class DuplicateEnvironment(Exception):
    def __str__(self):
        return "Duplicate environment %r. It has conflicting sets of data: %r != %r." % self.args

    __repr__ = __str__


class Reducer(object):
    def __init__(self, entry):
        kind, variable, pattern = entry
        assert kind in "&!"
        self.kind = kind
        self.is_exclude = kind == '!'
        self.variable = variable
        self.pattern = pattern

    def __str__(self):
        return "%s(%s[%s])" % (
            "exclude" if self.is_exclude else "include",
            self.variable,
            self.pattern,
        )

    __repr__ = __str__


class Entry(object):
    def __init__(self, value):
        value = value.strip()
        if not value:
            self.alias = ''
            self.value = ''
            self.merge = False
            self.reducers = []
        else:
            m = entry_rx.match(value)
            if not m:
                raise ValueError("Failed to parse %r" % value)
            m = m.groupdict()
            self.alias = m['alias']
            self.value = m['value']
            self.merge = m['merge']
            self.reducers = [Reducer(i) for i in reducer_rx.findall(m['reducers'] or '')]

        if self.value == '-':
            self.value = ''

        if self.alias is None:
            self.alias = special_chars_rx.sub('_', self.value)

    def __eq__(self, other):
        return self.alias == other.alias

    def __str__(self):
        return "Entry(%r, %salias=%r)" % (
            self.value,
            ', '.join(str(i) for i in self.reducers) + ', ' if self.reducers else '',
            self.alias,
        )

    __repr__ = __str__


def parse_config(fp, section='matrix'):
    parser = ConfigParser()
    parser.readfp(fp)
    config = OrderedDict()
    for name, value in parser.items(section):
        entries = config[name] = []
        for line in value.strip().splitlines():
            entry = Entry(line)
            duplicates = [i for i in entries if i == entry]
            if duplicates:
                warnings.warn(DuplicateEntry(entry, line, duplicates), DuplicateEntry, 1)
            entries.append(entry)
        if not entries:
            entries.append(Entry('-'))
    return config


[docs]def from_config(config): """ Generate a matrix from a configuration dictionary. """ matrix = {} variables = config.keys() for entries in product(*config.values()): combination = dict(zip(variables, entries)) include = True for value in combination.values(): for reducer in value.reducers: if reducer.pattern == '-': match = not combination[reducer.variable].value else: match = fnmatch(combination[reducer.variable].value, reducer.pattern) if match if reducer.is_exclude else not match: include = False if include: key = '-'.join(entry.alias for entry in entries if entry.alias) data = dict( zip(variables, (entry.value for entry in entries)) ) if key in matrix and data != matrix[key]: raise DuplicateEnvironment(key, data, matrix[key]) matrix[key] = data return matrix
[docs]def from_file(filename, section='matrix'): """ Generate a matrix from a .ini file. Configuration is expected to be in a ``[matrix]`` section. """ config = parse_config(open(filename), section=section) return from_config(config)
[docs]def from_string(string, section='matrix'): """ Generate a matrix from a .ini file. Configuration is expected to be in a ``[matrix]`` section. """ config = parse_config(StringIO(string), section=section) return from_config(config)