diff options
Diffstat (limited to 'tox/config.py')
-rw-r--r-- | tox/config.py | 1247 |
1 files changed, 0 insertions, 1247 deletions
diff --git a/tox/config.py b/tox/config.py deleted file mode 100644 index d6b4c52..0000000 --- a/tox/config.py +++ /dev/null @@ -1,1247 +0,0 @@ -import argparse -import os -import random -from fnmatch import fnmatchcase -import sys -import re -import shlex -import string -import pkg_resources -import itertools -import pluggy - -import tox.interpreters -from tox import hookspecs -from tox._verlib import NormalizedVersion - -import py - -import tox - -iswin32 = sys.platform == "win32" - -default_factors = {'jython': 'jython', 'pypy': 'pypy', 'pypy3': 'pypy3', - 'py': sys.executable} -for version in '26,27,32,33,34,35,36'.split(','): - default_factors['py' + version] = 'python%s.%s' % tuple(version) - -hookimpl = pluggy.HookimplMarker("tox") - -_dummy = object() - - -def get_plugin_manager(plugins=()): - # initialize plugin manager - import tox.venv - pm = pluggy.PluginManager("tox") - pm.add_hookspecs(hookspecs) - pm.register(tox.config) - pm.register(tox.interpreters) - pm.register(tox.venv) - pm.register(tox.session) - pm.load_setuptools_entrypoints("tox") - for plugin in plugins: - pm.register(plugin) - pm.check_pending() - return pm - - -class Parser: - """ command line and ini-parser control object. """ - - def __init__(self): - self.argparser = argparse.ArgumentParser( - description="tox options", add_help=False) - self._testenv_attr = [] - - def add_argument(self, *args, **kwargs): - """ add argument to command line parser. This takes the - same arguments that ``argparse.ArgumentParser.add_argument``. - """ - return self.argparser.add_argument(*args, **kwargs) - - def add_testenv_attribute(self, name, type, help, default=None, postprocess=None): - """ add an ini-file variable for "testenv" section. - - Types are specified as strings like "bool", "line-list", "string", "argv", "path", - "argvlist". - - The ``postprocess`` function will be called for each testenv - like ``postprocess(testenv_config=testenv_config, value=value)`` - where ``value`` is the value as read from the ini (or the default value) - and ``testenv_config`` is a :py:class:`tox.config.TestenvConfig` instance - which will receive all ini-variables as object attributes. - - Any postprocess function must return a value which will then be set - as the final value in the testenv section. - """ - self._testenv_attr.append(VenvAttribute(name, type, default, help, postprocess)) - - def add_testenv_attribute_obj(self, obj): - """ add an ini-file variable as an object. - - This works as the ``add_testenv_attribute`` function but expects - "name", "type", "help", and "postprocess" attributes on the object. - """ - assert hasattr(obj, "name") - assert hasattr(obj, "type") - assert hasattr(obj, "help") - assert hasattr(obj, "postprocess") - self._testenv_attr.append(obj) - - def _parse_args(self, args): - return self.argparser.parse_args(args) - - def _format_help(self): - return self.argparser.format_help() - - -class VenvAttribute: - def __init__(self, name, type, default, help, postprocess): - self.name = name - self.type = type - self.default = default - self.help = help - self.postprocess = postprocess - - -class DepOption: - name = "deps" - type = "line-list" - help = "each line specifies a dependency in pip/setuptools format." - default = () - - def postprocess(self, testenv_config, value): - deps = [] - config = testenv_config.config - for depline in value: - m = re.match(r":(\w+):\s*(\S+)", depline) - if m: - iname, name = m.groups() - ixserver = config.indexserver[iname] - else: - name = depline.strip() - ixserver = None - name = self._replace_forced_dep(name, config) - deps.append(DepConfig(name, ixserver)) - return deps - - def _replace_forced_dep(self, name, config): - """ - Override the given dependency config name taking --force-dep-version - option into account. - - :param name: dep config, for example ["pkg==1.0", "other==2.0"]. - :param config: Config instance - :return: the new dependency that should be used for virtual environments - """ - if not config.option.force_dep: - return name - for forced_dep in config.option.force_dep: - if self._is_same_dep(forced_dep, name): - return forced_dep - return name - - @classmethod - def _is_same_dep(cls, dep1, dep2): - """ - Returns True if both dependency definitions refer to the - same package, even if versions differ. - """ - dep1_name = pkg_resources.Requirement.parse(dep1).project_name - try: - dep2_name = pkg_resources.Requirement.parse(dep2).project_name - except pkg_resources.RequirementParseError: - # we couldn't parse a version, probably a URL - return False - return dep1_name == dep2_name - - -class PosargsOption: - name = "args_are_paths" - type = "bool" - default = True - help = "treat positional args in commands as paths" - - def postprocess(self, testenv_config, value): - config = testenv_config.config - args = config.option.args - if args: - if value: - args = [] - for arg in config.option.args: - if arg: - origpath = config.invocationcwd.join(arg, abs=True) - if origpath.check(): - arg = testenv_config.changedir.bestrelpath(origpath) - args.append(arg) - testenv_config._reader.addsubstitutions(args) - return value - - -class InstallcmdOption: - name = "install_command" - type = "argv" - default = "pip install {opts} {packages}" - help = "install command for dependencies and package under test." - - def postprocess(self, testenv_config, value): - if '{packages}' not in value: - raise tox.exception.ConfigError( - "'install_command' must contain '{packages}' substitution") - return value - - -def parseconfig(args=None, plugins=()): - """ - :param list[str] args: Optional list of arguments. - :type pkg: str - :rtype: :class:`Config` - :raise SystemExit: toxinit file is not found - """ - - pm = get_plugin_manager(plugins) - - if args is None: - args = sys.argv[1:] - - # prepare command line options - parser = Parser() - pm.hook.tox_addoption(parser=parser) - - # parse command line options - option = parser._parse_args(args) - interpreters = tox.interpreters.Interpreters(hook=pm.hook) - config = Config(pluginmanager=pm, option=option, interpreters=interpreters) - config._parser = parser - config._testenv_attr = parser._testenv_attr - - # parse ini file - basename = config.option.configfile - if os.path.isabs(basename): - inipath = py.path.local(basename) - else: - for path in py.path.local().parts(reverse=True): - inipath = path.join(basename) - if inipath.check(): - break - else: - inipath = py.path.local().join('setup.cfg') - if not inipath.check(): - feedback("toxini file %r not found" % (basename), sysexit=True) - - try: - parseini(config, inipath) - except tox.exception.InterpreterNotFound: - exn = sys.exc_info()[1] - # Use stdout to match test expectations - py.builtin.print_("ERROR: " + str(exn)) - - # post process config object - pm.hook.tox_configure(config=config) - - return config - - -def feedback(msg, sysexit=False): - py.builtin.print_("ERROR: " + msg, file=sys.stderr) - if sysexit: - raise SystemExit(1) - - -class VersionAction(argparse.Action): - def __call__(self, argparser, *args, **kwargs): - version = tox.__version__ - py.builtin.print_("%s imported from %s" % (version, tox.__file__)) - raise SystemExit(0) - - -class CountAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - if hasattr(namespace, self.dest): - setattr(namespace, self.dest, int(getattr(namespace, self.dest)) + 1) - else: - setattr(namespace, self.dest, 0) - - -class SetenvDict: - def __init__(self, dict, reader): - self.reader = reader - self.definitions = dict - self.resolved = {} - self._lookupstack = [] - - def __contains__(self, name): - return name in self.definitions - - def get(self, name, default=None): - try: - return self.resolved[name] - except KeyError: - try: - if name in self._lookupstack: - raise KeyError(name) - val = self.definitions[name] - except KeyError: - return os.environ.get(name, default) - self._lookupstack.append(name) - try: - self.resolved[name] = res = self.reader._replace(val) - finally: - self._lookupstack.pop() - return res - - def __getitem__(self, name): - x = self.get(name, _dummy) - if x is _dummy: - raise KeyError(name) - return x - - def keys(self): - return self.definitions.keys() - - def __setitem__(self, name, value): - self.definitions[name] = value - self.resolved[name] = value - - -@hookimpl -def tox_addoption(parser): - # formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--version", nargs=0, action=VersionAction, - dest="version", - help="report version information to stdout.") - parser.add_argument("-h", "--help", action="store_true", dest="help", - help="show help about options") - parser.add_argument("--help-ini", "--hi", action="store_true", dest="helpini", - help="show help about ini-names") - parser.add_argument("-v", nargs=0, action=CountAction, default=0, - dest="verbosity", - help="increase verbosity of reporting output.") - parser.add_argument("--showconfig", action="store_true", - help="show configuration information for all environments. ") - parser.add_argument("-l", "--listenvs", action="store_true", - dest="listenvs", help="show list of test environments") - parser.add_argument("-c", action="store", default="tox.ini", - dest="configfile", - help="use the specified config file name.") - parser.add_argument("-e", action="append", dest="env", - metavar="envlist", - help="work against specified environments (ALL selects all).") - parser.add_argument("--notest", action="store_true", dest="notest", - help="skip invoking test commands.") - parser.add_argument("--sdistonly", action="store_true", dest="sdistonly", - help="only perform the sdist packaging activity.") - parser.add_argument("--installpkg", action="store", default=None, - metavar="PATH", - help="use specified package for installation into venv, instead of " - "creating an sdist.") - parser.add_argument("--develop", action="store_true", dest="develop", - help="install package in the venv using 'setup.py develop' via " - "'pip -e .'") - parser.add_argument('-i', action="append", - dest="indexurl", metavar="URL", - help="set indexserver url (if URL is of form name=url set the " - "url for the 'name' indexserver, specifically)") - parser.add_argument("--pre", action="store_true", dest="pre", - help="install pre-releases and development versions of dependencies. " - "This will pass the --pre option to install_command " - "(pip by default).") - parser.add_argument("-r", "--recreate", action="store_true", - dest="recreate", - help="force recreation of virtual environments") - parser.add_argument("--result-json", action="store", - dest="resultjson", metavar="PATH", - help="write a json file with detailed information " - "about all commands and results involved.") - - # We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED. - parser.add_argument("--hashseed", action="store", - metavar="SEED", default=None, - help="set PYTHONHASHSEED to SEED before running commands. " - "Defaults to a random integer in the range [1, 4294967295] " - "([1, 1024] on Windows). " - "Passing 'noset' suppresses this behavior.") - parser.add_argument("--force-dep", action="append", - metavar="REQ", default=None, - help="Forces a certain version of one of the dependencies " - "when configuring the virtual environment. REQ Examples " - "'pytest<2.7' or 'django>=1.6'.") - parser.add_argument("--sitepackages", action="store_true", - help="override sitepackages setting to True in all envs") - parser.add_argument("--skip-missing-interpreters", action="store_true", - help="don't fail tests for missing interpreters") - parser.add_argument("--workdir", action="store", - dest="workdir", metavar="PATH", default=None, - help="tox working directory") - - parser.add_argument("args", nargs="*", - help="additional arguments available to command positional substitution") - - parser.add_testenv_attribute( - name="envdir", type="path", default="{toxworkdir}/{envname}", - help="venv directory") - - # add various core venv interpreter attributes - def setenv(testenv_config, value): - setenv = value - config = testenv_config.config - if "PYTHONHASHSEED" not in setenv and config.hashseed is not None: - setenv['PYTHONHASHSEED'] = config.hashseed - return setenv - - parser.add_testenv_attribute( - name="setenv", type="dict_setenv", postprocess=setenv, - help="list of X=Y lines with environment variable settings") - - def basepython_default(testenv_config, value): - if value is None: - for f in testenv_config.factors: - if f in default_factors: - return default_factors[f] - return sys.executable - return str(value) - - parser.add_testenv_attribute( - name="basepython", type="string", default=None, postprocess=basepython_default, - help="executable name or path of interpreter used to create a " - "virtual test environment.") - - parser.add_testenv_attribute( - name="envtmpdir", type="path", default="{envdir}/tmp", - help="venv temporary directory") - - parser.add_testenv_attribute( - name="envlogdir", type="path", default="{envdir}/log", - help="venv log directory") - - def downloadcache(testenv_config, value): - if value: - # env var, if present, takes precedence - downloadcache = os.environ.get("PIP_DOWNLOAD_CACHE", value) - return py.path.local(downloadcache) - - parser.add_testenv_attribute( - name="downloadcache", type="string", default=None, postprocess=downloadcache, - help="(deprecated) set PIP_DOWNLOAD_CACHE.") - - parser.add_testenv_attribute( - name="changedir", type="path", default="{toxinidir}", - help="directory to change to when running commands") - - parser.add_testenv_attribute_obj(PosargsOption()) - - parser.add_testenv_attribute( - name="skip_install", type="bool", default=False, - help="Do not install the current package. This can be used when " - "you need the virtualenv management but do not want to install " - "the current package") - - parser.add_testenv_attribute( - name="ignore_errors", type="bool", default=False, - help="if set to True all commands will be executed irrespective of their " - "result error status.") - - def recreate(testenv_config, value): - if testenv_config.config.option.recreate: - return True - return value - - parser.add_testenv_attribute( - name="recreate", type="bool", default=False, postprocess=recreate, - help="always recreate this test environment.") - - def passenv(testenv_config, value): - # Flatten the list to deal with space-separated values. - value = list( - itertools.chain.from_iterable( - [x.split(' ') for x in value])) - - passenv = set(["PATH", "PIP_INDEX_URL", "LANG", "LD_LIBRARY_PATH"]) - - # read in global passenv settings - p = os.environ.get("TOX_TESTENV_PASSENV", None) - if p is not None: - passenv.update(x for x in p.split() if x) - - # we ensure that tmp directory settings are passed on - # we could also set it to the per-venv "envtmpdir" - # but this leads to very long paths when run with jenkins - # so we just pass it on by default for now. - if sys.platform == "win32": - passenv.add("SYSTEMDRIVE") # needed for pip6 - passenv.add("SYSTEMROOT") # needed for python's crypto module - passenv.add("PATHEXT") # needed for discovering executables - passenv.add("TEMP") - passenv.add("TMP") - else: - passenv.add("TMPDIR") - for spec in value: - for name in os.environ: - if fnmatchcase(name.upper(), spec.upper()): - passenv.add(name) - return passenv - - parser.add_testenv_attribute( - name="passenv", type="line-list", postprocess=passenv, - help="environment variables needed during executing test commands " - "(taken from invocation environment). Note that tox always " - "passes through some basic environment variables which are " - "needed for basic functioning of the Python system. " - "See --showconfig for the eventual passenv setting.") - - parser.add_testenv_attribute( - name="whitelist_externals", type="line-list", - help="each lines specifies a path or basename for which tox will not warn " - "about it coming from outside the test environment.") - - parser.add_testenv_attribute( - name="platform", type="string", default=".*", - help="regular expression which must match against ``sys.platform``. " - "otherwise testenv will be skipped.") - - def sitepackages(testenv_config, value): - return testenv_config.config.option.sitepackages or value - - parser.add_testenv_attribute( - name="sitepackages", type="bool", default=False, postprocess=sitepackages, - help="Set to ``True`` if you want to create virtual environments that also " - "have access to globally installed packages.") - - def pip_pre(testenv_config, value): - return testenv_config.config.option.pre or value - - parser.add_testenv_attribute( - name="pip_pre", type="bool", default=False, postprocess=pip_pre, - help="If ``True``, adds ``--pre`` to the ``opts`` passed to " - "the install command. ") - - def develop(testenv_config, value): - option = testenv_config.config.option - return not option.installpkg and (value or option.develop) - - parser.add_testenv_attribute( - name="usedevelop", type="bool", postprocess=develop, default=False, - help="install package in develop/editable mode") - - parser.add_testenv_attribute_obj(InstallcmdOption()) - - parser.add_testenv_attribute( - name="list_dependencies_command", - type="argv", - default="pip freeze", - help="list dependencies for a virtual environment") - - parser.add_testenv_attribute_obj(DepOption()) - - parser.add_testenv_attribute( - name="commands", type="argvlist", default="", - help="each line specifies a test command and can use substitution.") - - parser.add_testenv_attribute( - "ignore_outcome", type="bool", default=False, - help="if set to True a failing result of this testenv will not make " - "tox fail, only a warning will be produced") - - -class Config(object): - """ Global Tox config object. """ - def __init__(self, pluginmanager, option, interpreters): - #: dictionary containing envname to envconfig mappings - self.envconfigs = {} - self.invocationcwd = py.path.local() - self.interpreters = interpreters - self.pluginmanager = pluginmanager - #: option namespace containing all parsed command line options - self.option = option - - @property - def homedir(self): - homedir = get_homedir() - if homedir is None: - homedir = self.toxinidir # XXX good idea? - return homedir - - -class TestenvConfig: - """ Testenv Configuration object. - In addition to some core attributes/properties this config object holds all - per-testenv ini attributes as attributes, see "tox --help-ini" for an overview. - """ - def __init__(self, envname, config, factors, reader): - #: test environment name - self.envname = envname - #: global tox config object - self.config = config - #: set of factors - self.factors = factors - self._reader = reader - - def get_envbindir(self): - """ path to directory where scripts/binaries reside. """ - if (sys.platform == "win32" - and "jython" not in self.basepython - and "pypy" not in self.basepython): - return self.envdir.join("Scripts") - else: - return self.envdir.join("bin") - - @property - def envbindir(self): - return self.get_envbindir() - - @property - def envpython(self): - """ path to python executable. """ - return self.get_envpython() - - def get_envpython(self): - """ path to python/jython executable. """ - if "jython" in str(self.basepython): - name = "jython" - else: - name = "python" - return self.envbindir.join(name) - - def get_envsitepackagesdir(self): - """ return sitepackagesdir of the virtualenv environment. - (only available during execution, not parsing) - """ - x = self.config.interpreters.get_sitepackagesdir( - info=self.python_info, - envdir=self.envdir) - return x - - @property - def python_info(self): - """ return sitepackagesdir of the virtualenv environment. """ - return self.config.interpreters.get_info(envconfig=self) - - def getsupportedinterpreter(self): - if sys.platform == "win32" and self.basepython and \ - "jython" in self.basepython: - raise tox.exception.UnsupportedInterpreter( - "Jython/Windows does not support installing scripts") - info = self.config.interpreters.get_info(envconfig=self) - if not info.executable: - raise tox.exception.InterpreterNotFound(self.basepython) - if not info.version_info: - raise tox.exception.InvocationError( - 'Failed to get version_info for %s: %s' % (info.name, info.err)) - if info.version_info < (2, 6): - raise tox.exception.UnsupportedInterpreter( - "python2.5 is not supported anymore, sorry") - return info.executable - - -testenvprefix = "testenv:" - - -def get_homedir(): - try: - return py.path.local._gethomedir() - except Exception: - return None - - -def make_hashseed(): - max_seed = 4294967295 - if sys.platform == 'win32': - max_seed = 1024 - return str(random.randint(1, max_seed)) - - -class parseini: - def __init__(self, config, inipath): - config.toxinipath = inipath - config.toxinidir = config.toxinipath.dirpath() - - self._cfg = py.iniconfig.IniConfig(config.toxinipath) - config._cfg = self._cfg - self.config = config - - if inipath.basename == 'setup.cfg': - prefix = 'tox' - else: - prefix = None - ctxname = getcontextname() - if ctxname == "jenkins": - reader = SectionReader("tox:jenkins", self._cfg, prefix=prefix, - fallbacksections=['tox']) - distshare_default = "{toxworkdir}/distshare" - elif not ctxname: - reader = SectionReader("tox", self._cfg, prefix=prefix) - distshare_default = "{homedir}/.tox/distshare" - else: - raise ValueError("invalid context") - - if config.option.hashseed is None: - hashseed = make_hashseed() - elif config.option.hashseed == 'noset': - hashseed = None - else: - hashseed = config.option.hashseed - config.hashseed = hashseed - - reader.addsubstitutions(toxinidir=config.toxinidir, - homedir=config.homedir) - # As older versions of tox may have bugs or incompatabilities that - # prevent parsing of tox.ini this must be the first thing checked. - config.minversion = reader.getstring("minversion", None) - if config.minversion: - minversion = NormalizedVersion(self.config.minversion) - toxversion = NormalizedVersion(tox.__version__) - if toxversion < minversion: - raise tox.exception.MinVersionError( - "tox version is %s, required is at least %s" % ( - toxversion, minversion)) - if config.option.workdir is None: - config.toxworkdir = reader.getpath("toxworkdir", "{toxinidir}/.tox") - else: - config.toxworkdir = config.toxinidir.join(config.option.workdir, abs=True) - - if not config.option.skip_missing_interpreters: - config.option.skip_missing_interpreters = \ - reader.getbool("skip_missing_interpreters", False) - - # determine indexserver dictionary - config.indexserver = {'default': IndexServerConfig('default')} - prefix = "indexserver" - for line in reader.getlist(prefix): - name, url = map(lambda x: x.strip(), line.split("=", 1)) - config.indexserver[name] = IndexServerConfig(name, url) - - override = False - if config.option.indexurl: - for urldef in config.option.indexurl: - m = re.match(r"\W*(\w+)=(\S+)", urldef) - if m is None: - url = urldef - name = "default" - else: - name, url = m.groups() - if not url: - url = None - if name != "ALL": - config.indexserver[name].url = url - else: - override = url - # let ALL override all existing entries - if override: - for name in config.indexserver: - config.indexserver[name] = IndexServerConfig(name, override) - - reader.addsubstitutions(toxworkdir=config.toxworkdir) - config.distdir = reader.getpath("distdir", "{toxworkdir}/dist") - reader.addsubstitutions(distdir=config.distdir) - config.distshare = reader.getpath("distshare", distshare_default) - reader.addsubstitutions(distshare=config.distshare) - config.sdistsrc = reader.getpath("sdistsrc", None) - config.setupdir = reader.getpath("setupdir", "{toxinidir}") - config.logdir = config.toxworkdir.join("log") - - config.envlist, all_envs = self._getenvdata(reader) - - # factors used in config or predefined - known_factors = self._list_section_factors("testenv") - known_factors.update(default_factors) - known_factors.add("python") - - # factors stated in config envlist - stated_envlist = reader.getstring("envlist", replace=False) - if stated_envlist: - for env in _split_env(stated_envlist): - known_factors.update(env.split('-')) - - # configure testenvs - for name in all_envs: - section = testenvprefix + name - factors = set(name.split('-')) - if section in self._cfg or factors <= known_factors: - config.envconfigs[name] = \ - self.make_envconfig(name, section, reader._subs, config) - - all_develop = all(name in config.envconfigs - and config.envconfigs[name].usedevelop - for name in config.envlist) - - config.skipsdist = reader.getbool("skipsdist", all_develop) - - def _list_section_factors(self, section): - factors = set() - if section in self._cfg: - for _, value in self._cfg[section].items(): - exprs = re.findall(r'^([\w{}\.,-]+)\:\s+', value, re.M) - factors.update(*mapcat(_split_factor_expr, exprs)) - return factors - - def make_envconfig(self, name, section, subs, config): - factors = set(name.split('-')) - reader = SectionReader(section, self._cfg, fallbacksections=["testenv"], - factors=factors) - vc = TestenvConfig(config=config, envname=name, factors=factors, reader=reader) - reader.addsubstitutions(**subs) - reader.addsubstitutions(envname=name) - reader.addsubstitutions(envbindir=vc.get_envbindir, - envsitepackagesdir=vc.get_envsitepackagesdir, - envpython=vc.get_envpython) - - for env_attr in config._testenv_attr: - atype = env_attr.type - if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"): - meth = getattr(reader, "get" + atype) - res = meth(env_attr.name, env_attr.default) - elif atype == "space-separated-list": - res = reader.getlist(env_attr.name, sep=" ") - elif atype == "line-list": - res = reader.getlist(env_attr.name, sep="\n") - else: - raise ValueError("unknown type %r" % (atype,)) - - if env_attr.postprocess: - res = env_attr.postprocess(testenv_config=vc, value=res) - setattr(vc, env_attr.name, res) - - if atype == "path": - reader.addsubstitutions(**{env_attr.name: res}) - - return vc - - def _getenvdata(self, reader): - envstr = self.config.option.env \ - or os.environ.get("TOXENV") \ - or reader.getstring("envlist", replace=False) \ - or [] - envlist = _split_env(envstr) - - # collect section envs - all_envs = set(envlist) - set(["ALL"]) - for section in self._cfg: - if section.name.startswith(testenvprefix): - all_envs.add(section.name[len(testenvprefix):]) - if not all_envs: - all_envs.add("python") - - if not envlist or "ALL" in envlist: - envlist = sorted(all_envs) - - return envlist, all_envs - - -def _split_env(env): - """if handed a list, action="append" was used for -e """ - if not isinstance(env, list): - if '\n' in env: - env = ','.join(env.split('\n')) - env = [env] - return mapcat(_expand_envstr, env) - - -def _split_factor_expr(expr): - partial_envs = _expand_envstr(expr) - return [set(e.split('-')) for e in partial_envs] - - -def _expand_envstr(envstr): - # split by commas not in groups - tokens = re.split(r'((?:\{[^}]+\})+)|,', envstr) - envlist = [''.join(g).strip() - for k, g in itertools.groupby(tokens, key=bool) if k] - - def expand(env): - tokens = re.split(r'\{([^}]+)\}', env) - parts = [token.split(',') for token in tokens] - return [''.join(variant) for variant in itertools.product(*parts)] - - return mapcat(expand, envlist) - - -def mapcat(f, seq): - return list(itertools.chain.from_iterable(map(f, seq))) - - -class DepConfig: - def __init__(self, name, indexserver=None): - self.name = name - self.indexserver = indexserver - - def __str__(self): - if self.indexserver: - if self.indexserver.name == "default": - return self.name - return ":%s:%s" % (self.indexserver.name, self.name) - return str(self.name) - __repr__ = __str__ - - -class IndexServerConfig: - def __init__(self, name, url=None): - self.name = name - self.url = url - - -#: Check value matches substitution form -#: of referencing value from other section. E.g. {[base]commands} -is_section_substitution = re.compile("{\[[^{}\s]+\]\S+?}").match - - -class SectionReader: - def __init__(self, section_name, cfgparser, fallbacksections=None, - factors=(), prefix=None): - if prefix is None: - self.section_name = section_name - else: - self.section_name = "%s:%s" % (prefix, section_name) - self._cfg = cfgparser - self.fallbacksections = fallbacksections or [] - self.factors = factors - self._subs = {} - self._subststack = [] - self._setenv = None - - def get_environ_value(self, name): - if self._setenv is None: - return os.environ.get(name) - return self._setenv.get(name) - - def addsubstitutions(self, _posargs=None, **kw): - self._subs.update(kw) - if _posargs: - self.posargs = _posargs - - def getpath(self, name, defaultpath): - toxinidir = self._subs['toxinidir'] - path = self.getstring(name, defaultpath) - if path is not None: - return toxinidir.join(path, abs=True) - - def getlist(self, name, sep="\n"): - s = self.getstring(name, None) - if s is None: - return [] - return [x.strip() for x in s.split(sep) if x.strip()] - - def getdict(self, name, default=None, sep="\n"): - value = self.getstring(name, None) - return self._getdict(value, default=default, sep=sep) - - def getdict_setenv(self, name, default=None, sep="\n"): - value = self.getstring(name, None, replace=True, crossonly=True) - definitions = self._getdict(value, default=default, sep=sep) - self._setenv = SetenvDict(definitions, reader=self) - return self._setenv - - def _getdict(self, value, default, sep): - if value is None: - return default or {} - - d = {} - for line in value.split(sep): - if line.strip(): - name, rest = line.split('=', 1) - d[name.strip()] = rest.strip() - - return d - - def getbool(self, name, default=None): - s = self.getstring(name, default) - if not s: - s = default - if s is None: - raise KeyError("no config value [%s] %s found" % ( - self.section_name, name)) - - if not isinstance(s, bool): - if s.lower() == "true": - s = True - elif s.lower() == "false": - s = False - else: - raise tox.exception.ConfigError( - "boolean value %r needs to be 'True' or 'False'") - return s - - def getargvlist(self, name, default=""): - s = self.getstring(name, default, replace=False) - return _ArgvlistReader.getargvlist(self, s) - - def getargv(self, name, default=""): - return self.getargvlist(name, default)[0] - - def getstring(self, name, default=None, replace=True, crossonly=False): - x = None - for s in [self.section_name] + self.fallbacksections: - try: - x = self._cfg[s][name] - break - except KeyError: - continue - - if x is None: - x = default - else: - x = self._apply_factors(x) - - if replace and x and hasattr(x, 'replace'): - x = self._replace(x, name=name, crossonly=crossonly) - # print "getstring", self.section_name, name, "returned", repr(x) - return x - - def _apply_factors(self, s): - def factor_line(line): - m = re.search(r'^([\w{}\.,-]+)\:\s+(.+)', line) - if not m: - return line - - expr, line = m.groups() - if any(fs <= self.factors for fs in _split_factor_expr(expr)): - return line - - lines = s.strip().splitlines() - return '\n'.join(filter(None, map(factor_line, lines))) - - def _replace(self, value, name=None, section_name=None, crossonly=False): - if '{' not in value: - return value - - section_name = section_name if section_name else self.section_name - self._subststack.append((section_name, name)) - try: - return Replacer(self, crossonly=crossonly).do_replace(value) - finally: - assert self._subststack.pop() == (section_name, name) - - -class Replacer: - RE_ITEM_REF = re.compile( - r''' - (?<!\\)[{] - (?:(?P<sub_type>[^[:{}]+):)? # optional sub_type for special rules - (?P<substitution_value>[^,{}]*) # substitution key - [}] - ''', re.VERBOSE) - - def __init__(self, reader, crossonly=False): - self.reader = reader - self.crossonly = crossonly - - def do_replace(self, x): - return self.RE_ITEM_REF.sub(self._replace_match, x) - - def _replace_match(self, match): - g = match.groupdict() - sub_value = g['substitution_value'] - if self.crossonly: - if sub_value.startswith("["): - return self._substitute_from_other_section(sub_value) - # in crossonly we return all other hits verbatim - start, end = match.span() - return match.string[start:end] - - # special case: opts and packages. Leave {opts} and - # {packages} intact, they are replaced manually in - # _venv.VirtualEnv.run_install_command. - if sub_value in ('opts', 'packages'): - return '{%s}' % sub_value - - try: - sub_type = g['sub_type'] - except KeyError: - raise tox.exception.ConfigError( - "Malformed substitution; no substitution type provided") - - if sub_type == "env": - return self._replace_env(match) - if sub_type is not None: - raise tox.exception.ConfigError( - "No support for the %s substitution type" % sub_type) - return self._replace_substitution(match) - - def _replace_env(self, match): - match_value = match.group('substitution_value') - if not match_value: - raise tox.exception.ConfigError( - 'env: requires an environment variable name') - - default = None - envkey_split = match_value.split(':', 1) - - if len(envkey_split) is 2: - envkey, default = envkey_split - else: - envkey = match_value - - envvalue = self.reader.get_environ_value(envkey) - if envvalue is None: - if default is None: - raise tox.exception.ConfigError( - "substitution env:%r: unknown environment variable %r " - " or recursive definition." % - (envkey, envkey)) - return default - return envvalue - - def _substitute_from_other_section(self, key): - if key.startswith("[") and "]" in key: - i = key.find("]") - section, item = key[1:i], key[i + 1:] - cfg = self.reader._cfg - if section in cfg and item in cfg[section]: - if (section, item) in self.reader._subststack: - raise ValueError('%s already in %s' % ( - (section, item), self.reader._subststack)) - x = str(cfg[section][item]) - return self.reader._replace(x, name=item, section_name=section, - crossonly=self.crossonly) - - raise tox.exception.ConfigError( - "substitution key %r not found" % key) - - def _replace_substitution(self, match): - sub_key = match.group('substitution_value') - val = self.reader._subs.get(sub_key, None) - if val is None: - val = self._substitute_from_other_section(sub_key) - if py.builtin.callable(val): - val = val() - return str(val) - - -class _ArgvlistReader: - @classmethod - def getargvlist(cls, reader, value): - """Parse ``commands`` argvlist multiline string. - - :param str name: Key name in a section. - :param str value: Content stored by key. - - :rtype: list[list[str]] - :raise :class:`tox.exception.ConfigError`: - line-continuation ends nowhere while resolving for specified section - """ - commands = [] - current_command = "" - for line in value.splitlines(): - line = line.rstrip() - if not line: - continue - if line.endswith("\\"): - current_command += " " + line[:-1] - continue - current_command += line - - if is_section_substitution(current_command): - replaced = reader._replace(current_command, crossonly=True) - commands.extend(cls.getargvlist(reader, replaced)) - else: - commands.append(cls.processcommand(reader, current_command)) - current_command = "" - else: - if current_command: - raise tox.exception.ConfigError( - "line-continuation ends nowhere while resolving for [%s] %s" % - (reader.section_name, "commands")) - return commands - - @classmethod - def processcommand(cls, reader, command): - posargs = getattr(reader, "posargs", None) - - # Iterate through each word of the command substituting as - # appropriate to construct the new command string. This - # string is then broken up into exec argv components using - # shlex. - newcommand = "" - for word in CommandParser(command).words(): - if word == "{posargs}" or word == "[]": - if posargs: - newcommand += " ".join(posargs) - continue - elif word.startswith("{posargs:") and word.endswith("}"): - if posargs: - newcommand += " ".join(posargs) - continue - else: - word = word[9:-1] - new_arg = "" - new_word = reader._replace(word) - new_word = reader._replace(new_word) - new_word = new_word.replace('\\{', '{').replace('\\}', '}') - new_arg += new_word - newcommand += new_arg - - # Construct shlex object that will not escape any values, - # use all values as is in argv. - shlexer = shlex.shlex(newcommand, posix=True) - shlexer.whitespace_split = True - shlexer.escape = '' - argv = list(shlexer) - return argv - - -class CommandParser(object): - - class State(object): - def __init__(self): - self.word = '' - self.depth = 0 - self.yield_words = [] - - def __init__(self, command): - self.command = command - - def words(self): - ps = CommandParser.State() - - def word_has_ended(): - return ((cur_char in string.whitespace and ps.word and - ps.word[-1] not in string.whitespace) or - (cur_char == '{' and ps.depth == 0 and not ps.word.endswith('\\')) or - (ps.depth == 0 and ps.word and ps.word[-1] == '}') or - (cur_char not in string.whitespace and ps.word and - ps.word.strip() == '')) - - def yield_this_word(): - yieldword = ps.word - ps.word = '' - if yieldword: - ps.yield_words.append(yieldword) - - def yield_if_word_ended(): - if word_has_ended(): - yield_this_word() - - def accumulate(): - ps.word += cur_char - - def push_substitution(): - ps.depth += 1 - - def pop_substitution(): - ps.depth -= 1 - - for cur_char in self.command: - if cur_char in string.whitespace: - if ps.depth == 0: - yield_if_word_ended() - accumulate() - elif cur_char == '{': - yield_if_word_ended() - accumulate() - push_substitution() - elif cur_char == '}': - accumulate() - pop_substitution() - else: - yield_if_word_ended() - accumulate() - - if ps.word.strip(): - yield_this_word() - return ps.yield_words - - -def getcontextname(): - if any(env in os.environ for env in ['JENKINS_URL', 'HUDSON_URL']): - return 'jenkins' - return None |