#!/usr/bin/env python # From: https://bitbucket.org/cdevienne/toxgen (pypi soon hopefully) and # modified slightly to work in python 2.6 and set some values that are not # being set. # # TODO(harlowja): remove me when toxgen is a pypi package. """ Produce a tox.ini file from a template config file. The template config file is a standard tox.ini file with additional sections. Theses sections will be combined to create new testenv: sections if they do not exists yet. """ import collections import itertools import optparse import os import six from six.moves import configparser try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict HEADER = '# DO NOT EDIT THIS FILE - it is machine generated from %(filename)s' SKIP_VENVS = frozenset(['venv']) parser = optparse.OptionParser(epilog=__doc__) parser.add_option('-i', '--input', dest='input', default='tox-tmpl.ini', metavar='FILE') parser.add_option('-o', '--output', dest='output', default='tox.ini', metavar='FILE') class AxisItem(object): def __init__(self, axis, name, config): self.axis = axis self.isdefault = name[-1] == '*' self.name = name[:-1] if self.isdefault else name self.load(config) def load(self, config): sectionname = 'axis:%s:%s' % (self.axis.name, self.name) if config.has_section(sectionname): self.options = dict(config.items(sectionname)) else: self.options = dict() for name, value in self.axis.defaults.items(): if name not in self.options: self.options[name] = value class Axis(object): def __init__(self, name, config): self.name = name self.load(config) def load(self, config): self.items = dict() values = config.get('axes', self.name).split(',') if config.has_section('axis:%s' % self.name): self.defaults = dict( config.items('axis:%s' % self.name) ) else: self.defaults = {} for value in values: self.items[value.strip('*')] = AxisItem(self, value, config) def format_list(contents, max_len=80, sep=","): lines = [] for line in contents: if not lines: lines.append(line + ",") else: last_len = len(lines[-1]) if last_len + len(line) >= max_len: lines.append(str(line) + sep) else: lines[-1] = lines[-1] + str(line) + sep return "\n".join(lines).rstrip(",") def render(incfg, filename, adjust_envlist=True): test_envs = set() for s in incfg.sections(): if s.startswith("testenv:"): env = s[len("testenv:"):].strip() if env in SKIP_VENVS or not env: continue test_envs.add(env) test_envs = [s for s in test_envs if s] try: envlist = incfg.get("tox", 'envlist') envlist = [e.strip() for e in envlist.split(",")] envlist = set([e for e in envlist if e]) except (configparser.NoOptionError, configparser.NoSectionError): envlist = set() for e in test_envs: if e not in envlist: envlist.add(e) if not incfg.has_section("tox"): incfg.add_section("tox") incfg.set("tox", "envlist", format_list(list(sorted(envlist)), max_len=-1)) text = six.StringIO() incfg.write(text) contents = [ HEADER % {'filename': os.path.basename(filename)}, '', # Remove how configparser uses tabs instead of spaces, madness... text.getvalue().replace("\t", " " * 4), ] return "\n".join(contents) def compile_template(incfg): axes = dict() if incfg.has_section('axes'): for axis in incfg.options('axes'): axes[axis] = Axis(axis, incfg) out = configparser.ConfigParser(dict_type=OrderedDict) for section in incfg.sections(): if section == 'axes' or section.startswith('axis:'): continue out.add_section(section) for name, value in incfg.items(section): out.set(section, name, value) items = [axis.items.keys() for axis in axes.values()] for combination in itertools.product(*items): options = {} section_name = ( 'testenv:' + '-'.join([item for item in combination if item]) ) section_alt_name = ( 'testenv:' + '-'.join([ itemname for axis, itemname in zip(axes.values(), combination) if itemname and not axis.items[itemname].isdefault ]) ) if section_alt_name == section_name: section_alt_name = None axes_items = [ '%s:%s' % (axis, itemname) for axis, itemname in zip(axes, combination) ] for axis, itemname in zip(axes.values(), combination): axis_options = axis.items[itemname].options if 'constraints' in axis_options: constraints = axis_options['constraints'].split('\n') for c in constraints: if c.startswith('!') and c[1:] in axes_items: continue for name, value in axis_options.items(): if name in options: options[name] += value else: options[name] = value constraints = options.pop('constraints', '').split('\n') neg_constraints = [c[1:] for c in constraints if c and c[0] == '!'] if not set(neg_constraints).isdisjoint(axes_items): continue if not out.has_section(section_name): out.add_section(section_name) if (section_alt_name and not out.has_section(section_alt_name)): out.add_section(section_alt_name) for name, value in reversed(options.items()): if not out.has_option(section_name, name): out.set(section_name, name, value) if section_alt_name and not out.has_option(section_alt_name, name): out.set(section_alt_name, name, value) return out def main(): options, args = parser.parse_args() tmpl = configparser.ConfigParser() with open(options.input, 'rb') as fh: tmpl.readfp(fh, filename=options.input) with open(options.output, 'wb') as outfile: text = render(compile_template(tmpl), options.input) outfile.write(text) if __name__ == '__main__': main()