[Pytest-commit] commit/tox: 6 new changesets
commits-noreply at bitbucket.org
commits-noreply at bitbucket.org
Mon May 11 12:36:05 CEST 2015
6 new commits in tox:
https://bitbucket.org/hpk42/tox/commits/1de999d6ecf9/
Changeset: 1de999d6ecf9
Branch: pluggy
User: hpk42
Date: 2015-05-11 09:26:07+00:00
Summary: add changelog entry for pluggy
Affected #: 1 file
diff -r 5bf717febabda1de4f8db338e983832d54c25e59 -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -32,6 +32,9 @@
- fix issue240: allow to specify empty argument list without it being
rewritten to ".". Thanks Daniel Hahler.
+- introduce experimental (not much documented yet) plugin system
+ based on pytest's externalized "pluggy" system.
+ See tox/hookspecs.py for the current hooks.
1.9.2
-----------
https://bitbucket.org/hpk42/tox/commits/af0751f4403a/
Changeset: af0751f4403a
Branch: abort_by_default_when_a_command_fails
User: hpk42
Date: 2015-05-11 10:11:57+00:00
Summary: close branch
Affected #: 0 files
https://bitbucket.org/hpk42/tox/commits/7f1bcc90f673/
Changeset: 7f1bcc90f673
Branch: pluggy
User: hpk42
Date: 2015-05-11 10:06:39+00:00
Summary: refactor testenv section parser to work by registering ini attributes
at tox_addoption() time. Introduce new "--help-ini" or "--hi" option
to show all testenv variables.
Affected #: 7 files
diff -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc -r 7f1bcc90f673122444ce1efac01206da661c84ff CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -36,6 +36,10 @@
based on pytest's externalized "pluggy" system.
See tox/hookspecs.py for the current hooks.
+- introduce parser.add_testenv_attribute() to register an ini-variable
+ for testenv sections. Can be used from plugins through the
+ tox_add_option hook.
+
1.9.2
-----------
diff -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc -r 7f1bcc90f673122444ce1efac01206da661c84ff tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -18,7 +18,7 @@
assert config.toxworkdir.realpath() == tmpdir.join(".tox").realpath()
assert config.envconfigs['py1'].basepython == sys.executable
assert config.envconfigs['py1'].deps == []
- assert not config.envconfigs['py1'].platform
+ assert config.envconfigs['py1'].platform == ".*"
def test_config_parsing_multienv(self, tmpdir, newconfig):
config = newconfig([], """
@@ -92,12 +92,12 @@
"""
Ensure correct parseini._is_same_dep is working with a few samples.
"""
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>=2.0')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>2.0')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<2.0')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<=2.0')
- assert not parseini._is_same_dep('pkg_hello-world3==1.0', 'otherpkg>=2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>=2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<=2.0')
+ assert not DepOption._is_same_dep('pkg_hello-world3==1.0', 'otherpkg>=2.0')
class TestConfigPlatform:
@@ -219,8 +219,8 @@
commands =
echo {[section]key}
""")
- reader = IniReader(config._cfg)
- x = reader.getargvlist("testenv", "commands")
+ reader = SectionReader("testenv", config._cfg)
+ x = reader.getargvlist("commands")
assert x == [["echo", "whatever"]]
def test_command_substitution_from_other_section_multiline(self, newconfig):
@@ -244,8 +244,8 @@
# comment is omitted
echo {[base]commands}
""")
- reader = IniReader(config._cfg)
- x = reader.getargvlist("testenv", "commands")
+ reader = SectionReader("testenv", config._cfg)
+ x = reader.getargvlist("commands")
assert x == [
"cmd1 param11 param12".split(),
"cmd2 param21 param22".split(),
@@ -256,16 +256,16 @@
class TestIniParser:
- def test_getdefault_single(self, tmpdir, newconfig):
+ def test_getstring_single(self, tmpdir, newconfig):
config = newconfig("""
[section]
key=value
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("section", "key")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getstring("key")
assert x == "value"
- assert not reader.getdefault("section", "hello")
- x = reader.getdefault("section", "hello", "world")
+ assert not reader.getstring("hello")
+ x = reader.getstring("hello", "world")
assert x == "world"
def test_missing_substitution(self, tmpdir, newconfig):
@@ -273,40 +273,40 @@
[mydefault]
key2={xyz}
""")
- reader = IniReader(config._cfg, fallbacksections=['mydefault'])
+ reader = SectionReader("mydefault", config._cfg, fallbacksections=['mydefault'])
assert reader is not None
- py.test.raises(tox.exception.ConfigError,
- 'reader.getdefault("mydefault", "key2")')
+ with py.test.raises(tox.exception.ConfigError):
+ reader.getstring("key2")
- def test_getdefault_fallback_sections(self, tmpdir, newconfig):
+ def test_getstring_fallback_sections(self, tmpdir, newconfig):
config = newconfig("""
[mydefault]
key2=value2
[section]
key=value
""")
- reader = IniReader(config._cfg, fallbacksections=['mydefault'])
- x = reader.getdefault("section", "key2")
+ reader = SectionReader("section", config._cfg, fallbacksections=['mydefault'])
+ x = reader.getstring("key2")
assert x == "value2"
- x = reader.getdefault("section", "key3")
+ x = reader.getstring("key3")
assert not x
- x = reader.getdefault("section", "key3", "world")
+ x = reader.getstring("key3", "world")
assert x == "world"
- def test_getdefault_substitution(self, tmpdir, newconfig):
+ def test_getstring_substitution(self, tmpdir, newconfig):
config = newconfig("""
[mydefault]
key2={value2}
[section]
key={value}
""")
- reader = IniReader(config._cfg, fallbacksections=['mydefault'])
+ reader = SectionReader("section", config._cfg, fallbacksections=['mydefault'])
reader.addsubstitutions(value="newvalue", value2="newvalue2")
- x = reader.getdefault("section", "key2")
+ x = reader.getstring("key2")
assert x == "newvalue2"
- x = reader.getdefault("section", "key3")
+ x = reader.getstring("key3")
assert not x
- x = reader.getdefault("section", "key3", "{value2}")
+ x = reader.getstring("key3", "{value2}")
assert x == "newvalue2"
def test_getlist(self, tmpdir, newconfig):
@@ -316,9 +316,9 @@
item1
{item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="not", item2="grr")
- x = reader.getlist("section", "key2")
+ x = reader.getlist("key2")
assert x == ['item1', 'grr']
def test_getdict(self, tmpdir, newconfig):
@@ -328,28 +328,31 @@
key1=item1
key2={item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="not", item2="grr")
- x = reader.getdict("section", "key2")
+ x = reader.getdict("key2")
assert 'key1' in x
assert 'key2' in x
assert x['key1'] == 'item1'
assert x['key2'] == 'grr'
- def test_getdefault_environment_substitution(self, monkeypatch, newconfig):
+ x = reader.getdict("key3", {1: 2})
+ assert x == {1: 2}
+
+ def test_getstring_environment_substitution(self, monkeypatch, newconfig):
monkeypatch.setenv("KEY1", "hello")
config = newconfig("""
[section]
key1={env:KEY1}
key2={env:KEY2}
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("section", "key1")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getstring("key1")
assert x == "hello"
- py.test.raises(tox.exception.ConfigError,
- 'reader.getdefault("section", "key2")')
+ with py.test.raises(tox.exception.ConfigError):
+ reader.getstring("key2")
- def test_getdefault_environment_substitution_with_default(self, monkeypatch, newconfig):
+ def test_getstring_environment_substitution_with_default(self, monkeypatch, newconfig):
monkeypatch.setenv("KEY1", "hello")
config = newconfig("""
[section]
@@ -357,12 +360,12 @@
key2={env:KEY2:DEFAULT_VALUE}
key3={env:KEY3:}
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("section", "key1")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getstring("key1")
assert x == "hello"
- x = reader.getdefault("section", "key2")
+ x = reader.getstring("key2")
assert x == "DEFAULT_VALUE"
- x = reader.getdefault("section", "key3")
+ x = reader.getstring("key3")
assert x == ""
def test_value_matches_section_substituion(self):
@@ -373,15 +376,15 @@
assert is_section_substitution("{[setup]}") is None
assert is_section_substitution("{[setup] commands}") is None
- def test_getdefault_other_section_substitution(self, newconfig):
+ def test_getstring_other_section_substitution(self, newconfig):
config = newconfig("""
[section]
key = rue
[testenv]
key = t{[section]key}
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("testenv", "key")
+ reader = SectionReader("testenv", config._cfg)
+ x = reader.getstring("key")
assert x == "true"
def test_argvlist(self, tmpdir, newconfig):
@@ -391,12 +394,12 @@
cmd1 {item1} {item2}
cmd2 {item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="with space", item2="grr")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "with", "space", "grr"],
["cmd2", "grr"]]
@@ -405,9 +408,9 @@
[section]
comm = py.test {posargs}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions([r"hello\this"])
- argv = reader.getargv("section", "comm")
+ argv = reader.getargv("comm")
assert argv == ["py.test", "hello\\this"]
def test_argvlist_multiline(self, tmpdir, newconfig):
@@ -417,12 +420,12 @@
cmd1 {item1} \ # a comment
{item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="with space", item2="grr")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "with", "space", "grr"]]
def test_argvlist_quoting_in_command(self, tmpdir, newconfig):
@@ -432,8 +435,8 @@
cmd1 'with space' \ # a comment
'after the comment'
""")
- reader = IniReader(config._cfg)
- x = reader.getargvlist("section", "key1")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getargvlist("key1")
assert x == [["cmd1", "with space", "after the comment"]]
def test_argvlist_positional_substitution(self, tmpdir, newconfig):
@@ -444,22 +447,22 @@
cmd2 {posargs:{item2} \
other}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
posargs = ['hello', 'world']
reader.addsubstitutions(posargs, item2="value2")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- argvlist = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ argvlist = reader.getargvlist("key2")
assert argvlist[0] == ["cmd1"] + posargs
assert argvlist[1] == ["cmd2"] + posargs
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions([], item2="value2")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- argvlist = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ argvlist = reader.getargvlist("key2")
assert argvlist[0] == ["cmd1"]
assert argvlist[1] == ["cmd2", "value2", "other"]
@@ -471,10 +474,10 @@
cmd2 -f '{posargs}'
cmd3 -f {posargs}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(["foo", "bar"])
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "--foo-args=foo bar"],
["cmd2", "-f", "foo bar"],
["cmd3", "-f", "foo", "bar"]]
@@ -485,10 +488,10 @@
key2=
cmd1 -f {posargs}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(["foo", "'bar", "baz'"])
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "-f", "foo", "bar baz"]]
def test_positional_arguments_are_only_replaced_when_standing_alone(self, tmpdir, newconfig):
@@ -500,11 +503,11 @@
cmd2 -m '\'something\'' []
cmd3 something[]else
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
posargs = ['hello', 'world']
reader.addsubstitutions(posargs)
- argvlist = reader.getargvlist('section', 'key')
+ argvlist = reader.getargvlist('key')
assert argvlist[0] == ['cmd0'] + posargs
assert argvlist[1] == ['cmd1', '-m', '[abc]']
assert argvlist[2] == ['cmd2', '-m', "something"] + posargs
@@ -516,32 +519,32 @@
key = py.test -n5 --junitxml={envlogdir}/junit-{envname}.xml []
"""
config = newconfig(inisource)
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
posargs = ['hello', 'world']
reader.addsubstitutions(posargs, envlogdir='ENV_LOG_DIR', envname='ENV_NAME')
expected = [
'py.test', '-n5', '--junitxml=ENV_LOG_DIR/junit-ENV_NAME.xml', 'hello', 'world'
]
- assert reader.getargvlist('section', 'key')[0] == expected
+ assert reader.getargvlist('key')[0] == expected
def test_getargv(self, newconfig):
config = newconfig("""
[section]
key=some command "with quoting"
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
expected = ['some', 'command', 'with quoting']
- assert reader.getargv('section', 'key') == expected
+ assert reader.getargv('key') == expected
def test_getpath(self, tmpdir, newconfig):
config = newconfig("""
[section]
path1={HELLO}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(toxinidir=tmpdir, HELLO="mypath")
- x = reader.getpath("section", "path1", tmpdir)
+ x = reader.getpath("path1", tmpdir)
assert x == tmpdir.join("mypath")
def test_getbool(self, tmpdir, newconfig):
@@ -553,13 +556,13 @@
key2a=falsE
key5=yes
""")
- reader = IniReader(config._cfg)
- assert reader.getbool("section", "key1") is True
- assert reader.getbool("section", "key1a") is True
- assert reader.getbool("section", "key2") is False
- assert reader.getbool("section", "key2a") is False
- py.test.raises(KeyError, 'reader.getbool("section", "key3")')
- py.test.raises(tox.exception.ConfigError, 'reader.getbool("section", "key5")')
+ reader = SectionReader("section", config._cfg)
+ assert reader.getbool("key1") is True
+ assert reader.getbool("key1a") is True
+ assert reader.getbool("key2") is False
+ assert reader.getbool("key2a") is False
+ py.test.raises(KeyError, 'reader.getbool("key3")')
+ py.test.raises(tox.exception.ConfigError, 'reader.getbool("key5")')
class TestConfigTestEnv:
@@ -585,7 +588,7 @@
assert envconfig.commands == [["xyz", "--abc"]]
assert envconfig.changedir == config.setupdir
assert envconfig.sitepackages is False
- assert envconfig.develop is False
+ assert envconfig.usedevelop is False
assert envconfig.envlogdir == envconfig.envdir.join("log")
assert list(envconfig.setenv.keys()) == ['PYTHONHASHSEED']
hashseed = envconfig.setenv['PYTHONHASHSEED']
@@ -605,7 +608,7 @@
[testenv]
usedevelop = True
""")
- assert not config.envconfigs["python"].develop
+ assert not config.envconfigs["python"].usedevelop
def test_specific_command_overrides(self, tmpdir, newconfig):
config = newconfig("""
@@ -1371,7 +1374,7 @@
def test_noset(self, tmpdir, newconfig):
args = ['--hashseed', 'noset']
envconfig = self._get_envconfig(newconfig, args=args)
- assert envconfig.setenv is None
+ assert envconfig.setenv == {}
def test_noset_with_setenv(self, tmpdir, newconfig):
tox_ini = """
diff -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc -r 7f1bcc90f673122444ce1efac01206da661c84ff tests/test_venv.py
--- a/tests/test_venv.py
+++ b/tests/test_venv.py
@@ -464,7 +464,7 @@
venv = VirtualEnv(envconfig, session=mocksession)
venv.update()
cconfig = venv._getliveconfig()
- cconfig.develop = True
+ cconfig.usedevelop = True
cconfig.writeconfig(venv.path_config)
mocksession._clearmocks()
venv.update()
diff -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc -r 7f1bcc90f673122444ce1efac01206da661c84ff tox.ini
--- a/tox.ini
+++ b/tox.ini
@@ -5,8 +5,10 @@
commands=echo {posargs}
[testenv]
-commands=py.test --junitxml={envlogdir}/junit-{envname}.xml {posargs}
+commands= py.test --timeout=60 {posargs}
+
deps=pytest>=2.3.5
+ pytest-timeout
[testenv:docs]
basepython=python
@@ -14,11 +16,10 @@
deps=sphinx
{[testenv]deps}
commands=
- py.test -v \
- --junitxml={envlogdir}/junit-{envname}.xml \
- check_sphinx.py {posargs}
+ py.test -v check_sphinx.py {posargs}
[testenv:flakes]
+qwe = 123
deps = pytest-flakes>=0.2
pytest-pep8
diff -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc -r 7f1bcc90f673122444ce1efac01206da661c84ff tox/_cmdline.py
--- a/tox/_cmdline.py
+++ b/tox/_cmdline.py
@@ -25,12 +25,34 @@
def main(args=None):
try:
config = parseconfig(args)
+ if config.option.help:
+ show_help(config)
+ raise SystemExit(0)
+ elif config.option.helpini:
+ show_help_ini(config)
+ raise SystemExit(0)
retcode = Session(config).runcommand()
raise SystemExit(retcode)
except KeyboardInterrupt:
raise SystemExit(2)
+def show_help(config):
+ tw = py.io.TerminalWriter()
+ tw.write(config._parser.format_help())
+ tw.line()
+
+
+def show_help_ini(config):
+ tw = py.io.TerminalWriter()
+ tw.sep("-", "per-testenv attributes")
+ for env_attr in config._testenv_attr:
+ tw.line("%-15s %-8s default: %s" %
+ (env_attr.name, "<" + env_attr.type + ">", env_attr.default), bold=True)
+ tw.line(env_attr.help)
+ tw.line()
+
+
class Action(object):
def __init__(self, session, venv, msg, args):
self.venv = venv
@@ -487,7 +509,7 @@
venv.status = "platform mismatch"
continue # we simply omit non-matching platforms
if self.setupenv(venv):
- if venv.envconfig.develop:
+ if venv.envconfig.usedevelop:
self.developpkg(venv, self.config.setupdir)
elif self.config.skipsdist or venv.envconfig.skip_install:
self.finishvenv(venv)
@@ -566,7 +588,7 @@
self.report.line(" deps=%s" % envconfig.deps)
self.report.line(" envdir= %s" % envconfig.envdir)
self.report.line(" downloadcache=%s" % envconfig.downloadcache)
- self.report.line(" usedevelop=%s" % envconfig.develop)
+ self.report.line(" usedevelop=%s" % envconfig.usedevelop)
def showenvs(self):
for env in self.config.envlist:
diff -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc -r 7f1bcc90f673122444ce1efac01206da661c84ff tox/_config.py
--- a/tox/_config.py
+++ b/tox/_config.py
@@ -38,6 +38,122 @@
return pm
+class MyParser:
+ def __init__(self):
+ self.argparser = argparse.ArgumentParser(
+ description="tox options", add_help=False)
+ self._testenv_attr = []
+
+ def add_argument(self, *args, **kwargs):
+ return self.argparser.add_argument(*args, **kwargs)
+
+ def add_testenv_attribute(self, name, type, help, default=None, postprocess=None):
+ self._testenv_attr.append(VenvAttribute(name, type, default, help, postprocess))
+
+ def add_testenv_attribute_obj(self, obj):
+ 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, config, reader, section_val):
+ deps = []
+ for depline in section_val:
+ 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
+ dep2_name = pkg_resources.Requirement.parse(dep2).project_name
+ 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, config, reader, section_val):
+ args = config.option.args
+ if args:
+ if section_val:
+ args = []
+ for arg in config.option.args:
+ if arg:
+ origpath = config.invocationcwd.join(arg, abs=True)
+ if origpath.check():
+ arg = reader.getpath("changedir", ".").bestrelpath(origpath)
+ args.append(arg)
+ reader.addsubstitutions(args)
+ return section_val
+
+
+class InstallcmdOption:
+ name = "install_command"
+ type = "argv"
+ default = "pip install {opts} {packages}"
+ help = "install command for dependencies and package under test."
+
+ def postprocess(self, config, reader, section_val):
+ if '{packages}' not in section_val:
+ raise tox.exception.ConfigError(
+ "'install_command' must contain '{packages}' substitution")
+ return section_val
+
+
def parseconfig(args=None):
"""
:param list[str] args: Optional list of arguments.
@@ -52,13 +168,15 @@
args = sys.argv[1:]
# prepare command line options
- parser = argparse.ArgumentParser(description=__doc__)
+ parser = MyParser()
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
@@ -111,6 +229,10 @@
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.")
@@ -156,6 +278,7 @@
"all commands and results involved. This will turn off "
"pass-through output from running test commands which is "
"instead captured into the json result file.")
+
# We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED.
parser.add_argument("--hashseed", action="store",
metavar="SEED", default=None,
@@ -175,7 +298,130 @@
parser.add_argument("args", nargs="*",
help="additional arguments available to command positional substitution")
- return parser
+
+ # add various core venv interpreter attributes
+
+ parser.add_testenv_attribute(
+ name="envdir", type="path", default="{toxworkdir}/{envname}",
+ help="venv directory")
+
+ 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(config, reader, section_val):
+ if section_val:
+ # env var, if present, takes precedence
+ downloadcache = os.environ.get("PIP_DOWNLOAD_CACHE", section_val)
+ 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")
+
+ def recreate(config, reader, section_val):
+ if config.option.recreate:
+ return True
+ return section_val
+
+ parser.add_testenv_attribute(
+ name="recreate", type="bool", default=False, postprocess=recreate,
+ help="always recreate this test environment.")
+
+ def setenv(config, reader, section_val):
+ setenv = section_val
+ 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", postprocess=setenv,
+ help="list of X=Y lines with environment variable settings")
+
+ def passenv(config, reader, section_val):
+ passenv = set(["PATH"])
+ if sys.platform == "win32":
+ passenv.add("SYSTEMROOT") # needed for python's crypto module
+ passenv.add("PATHEXT") # needed for discovering executables
+ for spec in section_val:
+ for name in os.environ:
+ if fnmatchcase(name.upper(), spec.upper()):
+ passenv.add(name)
+ return passenv
+
+ parser.add_testenv_attribute(
+ name="passenv", type="space-separated-list", postprocess=passenv,
+ help="environment variables names which shall be passed "
+ "from tox invocation to test environment when executing commands.")
+
+ 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(config, reader, section_val):
+ return config.option.sitepackages or section_val
+
+ 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(config, reader, section_val):
+ return config.option.pre or section_val
+
+ 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(config, reader, section_val):
+ return not config.option.installpkg and (section_val or config.option.develop)
+
+ parser.add_testenv_attribute(
+ name="usedevelop", type="bool", postprocess=develop, default=False,
+ help="install package in develop/editable mode")
+
+ def basepython_default(config, reader, section_val):
+ if section_val is None:
+ for f in reader.factors:
+ if f in default_factors:
+ return default_factors[f]
+ return sys.executable
+ return str(section_val)
+
+ 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_obj(InstallcmdOption())
+ 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.")
class Config(object):
@@ -272,12 +518,10 @@
self.config = config
ctxname = getcontextname()
if ctxname == "jenkins":
- reader = IniReader(self._cfg, fallbacksections=['tox'])
- toxsection = "tox:%s" % ctxname
+ reader = SectionReader("tox:jenkins", self._cfg, fallbacksections=['tox'])
distshare_default = "{toxworkdir}/distshare"
elif not ctxname:
- reader = IniReader(self._cfg)
- toxsection = "tox"
+ reader = SectionReader("tox", self._cfg)
distshare_default = "{homedir}/.tox/distshare"
else:
raise ValueError("invalid context")
@@ -292,18 +536,17 @@
reader.addsubstitutions(toxinidir=config.toxinidir,
homedir=config.homedir)
- config.toxworkdir = reader.getpath(toxsection, "toxworkdir",
- "{toxinidir}/.tox")
- config.minversion = reader.getdefault(toxsection, "minversion", None)
+ config.toxworkdir = reader.getpath("toxworkdir", "{toxinidir}/.tox")
+ config.minversion = reader.getstring("minversion", None)
if not config.option.skip_missing_interpreters:
config.option.skip_missing_interpreters = \
- reader.getbool(toxsection, "skip_missing_interpreters", False)
+ reader.getbool("skip_missing_interpreters", False)
# determine indexserver dictionary
config.indexserver = {'default': IndexServerConfig('default')}
prefix = "indexserver"
- for line in reader.getlist(toxsection, prefix):
+ for line in reader.getlist(prefix):
name, url = map(lambda x: x.strip(), line.split("=", 1))
config.indexserver[name] = IndexServerConfig(name, url)
@@ -328,16 +571,15 @@
config.indexserver[name] = IndexServerConfig(name, override)
reader.addsubstitutions(toxworkdir=config.toxworkdir)
- config.distdir = reader.getpath(toxsection, "distdir", "{toxworkdir}/dist")
+ config.distdir = reader.getpath("distdir", "{toxworkdir}/dist")
reader.addsubstitutions(distdir=config.distdir)
- config.distshare = reader.getpath(toxsection, "distshare",
- distshare_default)
+ config.distshare = reader.getpath("distshare", distshare_default)
reader.addsubstitutions(distshare=config.distshare)
- config.sdistsrc = reader.getpath(toxsection, "sdistsrc", None)
- config.setupdir = reader.getpath(toxsection, "setupdir", "{toxinidir}")
+ 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, toxsection)
+ config.envlist, all_envs = self._getenvdata(reader)
# factors used in config or predefined
known_factors = self._list_section_factors("testenv")
@@ -345,7 +587,7 @@
known_factors.add("python")
# factors stated in config envlist
- stated_envlist = reader.getdefault(toxsection, "envlist", replace=False)
+ stated_envlist = reader.getstring("envlist", replace=False)
if stated_envlist:
for env in _split_env(stated_envlist):
known_factors.update(env.split('-'))
@@ -356,13 +598,13 @@
factors = set(name.split('-'))
if section in self._cfg or factors <= known_factors:
config.envconfigs[name] = \
- self._makeenvconfig(name, section, reader._subs, config)
+ self.make_envconfig(name, section, reader._subs, config)
all_develop = all(name in config.envconfigs
- and config.envconfigs[name].develop
+ and config.envconfigs[name].usedevelop
for name in config.envlist)
- config.skipsdist = reader.getbool(toxsection, "skipsdist", all_develop)
+ config.skipsdist = reader.getbool("skipsdist", all_develop)
def _list_section_factors(self, section):
factors = set()
@@ -372,115 +614,51 @@
factors.update(*mapcat(_split_factor_expr, exprs))
return factors
- def _makeenvconfig(self, name, section, subs, config):
+ def make_envconfig(self, name, section, subs, config):
vc = VenvConfig(config=config, envname=name)
factors = set(name.split('-'))
- reader = IniReader(self._cfg, fallbacksections=["testenv"], factors=factors)
+ reader = SectionReader(section, self._cfg, fallbacksections=["testenv"],
+ factors=factors)
reader.addsubstitutions(**subs)
- vc.develop = (
- not config.option.installpkg
- and reader.getbool(section, "usedevelop", config.option.develop))
- vc.envdir = reader.getpath(section, "envdir", "{toxworkdir}/%s" % name)
- vc.args_are_paths = reader.getbool(section, "args_are_paths", True)
- if reader.getdefault(section, "python", None):
- raise tox.exception.ConfigError(
- "'python=' key was renamed to 'basepython='")
- bp = next((default_factors[f] for f in factors if f in default_factors),
- sys.executable)
- vc.basepython = reader.getdefault(section, "basepython", bp)
+ reader.addsubstitutions(envname=name)
- reader.addsubstitutions(envdir=vc.envdir, envname=vc.envname,
- envbindir=vc.envbindir, envpython=vc.envpython,
- envsitepackagesdir=vc.envsitepackagesdir)
- vc.envtmpdir = reader.getpath(section, "tmpdir", "{envdir}/tmp")
- vc.envlogdir = reader.getpath(section, "envlogdir", "{envdir}/log")
- reader.addsubstitutions(envlogdir=vc.envlogdir, envtmpdir=vc.envtmpdir)
- vc.changedir = reader.getpath(section, "changedir", "{toxinidir}")
- if config.option.recreate:
- vc.recreate = True
- else:
- vc.recreate = reader.getbool(section, "recreate", False)
- args = config.option.args
- if args:
- if vc.args_are_paths:
- args = []
- for arg in config.option.args:
- if arg:
- origpath = config.invocationcwd.join(arg, abs=True)
- if origpath.check():
- arg = vc.changedir.bestrelpath(origpath)
- args.append(arg)
- reader.addsubstitutions(args)
- setenv = {}
- if config.hashseed is not None:
- setenv['PYTHONHASHSEED'] = config.hashseed
- setenv.update(reader.getdict(section, 'setenv'))
+ for env_attr in config._testenv_attr:
+ atype = env_attr.type
+ if atype in ("bool", "path", "string", "dict", "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,))
- # read passenv
- vc.passenv = set(["PATH"])
- if sys.platform == "win32":
- vc.passenv.add("SYSTEMROOT") # needed for python's crypto module
- vc.passenv.add("PATHEXT") # needed for discovering executables
- for spec in reader.getlist(section, "passenv", sep=" "):
- for name in os.environ:
- if fnmatchcase(name.lower(), spec.lower()):
- vc.passenv.add(name)
+ if env_attr.postprocess:
+ res = env_attr.postprocess(config, reader, res)
+ setattr(vc, env_attr.name, res)
- vc.setenv = setenv
- if not vc.setenv:
- vc.setenv = None
+ if atype == "path":
+ reader.addsubstitutions(**{env_attr.name: res})
- vc.commands = reader.getargvlist(section, "commands")
- vc.whitelist_externals = reader.getlist(section,
- "whitelist_externals")
- vc.deps = []
- for depline in reader.getlist(section, "deps"):
- 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)
- vc.deps.append(DepConfig(name, ixserver))
+ if env_attr.name == "install_command":
+ reader.addsubstitutions(envbindir=vc.envbindir, envpython=vc.envpython,
+ envsitepackagesdir=vc.envsitepackagesdir)
- platform = ""
- for platform in reader.getlist(section, "platform"):
- if platform.strip():
- break
- vc.platform = platform
-
- vc.sitepackages = (
- self.config.option.sitepackages
- or reader.getbool(section, "sitepackages", False))
-
- vc.downloadcache = None
- downloadcache = reader.getdefault(section, "downloadcache")
- if downloadcache:
- # env var, if present, takes precedence
- downloadcache = os.environ.get("PIP_DOWNLOAD_CACHE", downloadcache)
- vc.downloadcache = py.path.local(downloadcache)
-
- vc.install_command = reader.getargv(
- section,
- "install_command",
- "pip install {opts} {packages}",
- )
- if '{packages}' not in vc.install_command:
- raise tox.exception.ConfigError(
- "'install_command' must contain '{packages}' substitution")
- vc.pip_pre = config.option.pre or reader.getbool(
- section, "pip_pre", False)
-
- vc.skip_install = reader.getbool(section, "skip_install", False)
-
+ # XXX introduce some testenv verification like this:
+ # try:
+ # sec = self._cfg[section]
+ # except KeyError:
+ # sec = self._cfg["testenv"]
+ # for name in sec:
+ # if name not in names:
+ # print ("unknown testenv attribute: %r" % (name,))
return vc
- def _getenvdata(self, reader, toxsection):
+ def _getenvdata(self, reader):
envstr = self.config.option.env \
or os.environ.get("TOXENV") \
- or reader.getdefault(toxsection, "envlist", replace=False) \
+ or reader.getstring("envlist", replace=False) \
or []
envlist = _split_env(envstr)
@@ -497,32 +675,6 @@
return envlist, all_envs
- 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
- dep2_name = pkg_resources.Requirement.parse(dep2).project_name
- return dep1_name == dep2_name
-
def _split_env(env):
"""if handed a list, action="append" was used for -e """
@@ -589,8 +741,9 @@
re.VERBOSE)
-class IniReader:
- def __init__(self, cfgparser, fallbacksections=None, factors=()):
+class SectionReader:
+ def __init__(self, section_name, cfgparser, fallbacksections=None, factors=()):
+ self.section_name = section_name
self._cfg = cfgparser
self.fallbacksections = fallbacksections or []
self.factors = factors
@@ -602,129 +755,39 @@
if _posargs:
self.posargs = _posargs
- def getpath(self, section, name, defaultpath):
+ def getpath(self, name, defaultpath):
toxinidir = self._subs['toxinidir']
- path = self.getdefault(section, name, defaultpath)
+ path = self.getstring(name, defaultpath)
if path is None:
return path
return toxinidir.join(path, abs=True)
- def getlist(self, section, name, sep="\n"):
- s = self.getdefault(section, name, None)
+ 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, section, name, sep="\n"):
- s = self.getdefault(section, name, None)
+ def getdict(self, name, default=None, sep="\n"):
+ s = self.getstring(name, None)
if s is None:
- return {}
+ return default or {}
value = {}
for line in s.split(sep):
- if not line.strip():
- continue
- name, rest = line.split('=', 1)
- value[name.strip()] = rest.strip()
+ if line.strip():
+ name, rest = line.split('=', 1)
+ value[name.strip()] = rest.strip()
return value
- def getargvlist(self, section, name):
- """Get arguments for every parsed command.
-
- :param str section: Section name in the configuration.
- :param str name: Key name in a section.
- :rtype: list[list[str]]
- :raise :class:`tox.exception.ConfigError`:
- line-continuation ends nowhere while resolving for specified section
- """
- content = self.getdefault(section, name, '', replace=False)
- return self._parse_commands(section, name, content)
-
- def _parse_commands(self, section, name, content):
- """Parse commands from key content in specified section.
-
- :param str section: Section name in the configuration.
- :param str name: Key name in a section.
- :param str content: 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 content.splitlines():
- line = line.rstrip()
- i = line.find("#")
- if i != -1:
- line = line[:i].rstrip()
- if not line:
- continue
- if line.endswith("\\"):
- current_command += " " + line[:-1]
- continue
- current_command += line
-
- if is_section_substitution(current_command):
- replaced = self._replace(current_command)
- commands.extend(self._parse_commands(section, name, replaced))
- else:
- commands.append(self._processcommand(current_command))
- current_command = ""
- else:
- if current_command:
- raise tox.exception.ConfigError(
- "line-continuation ends nowhere while resolving for [%s] %s" %
- (section, name))
- return commands
-
- def _processcommand(self, command):
- posargs = getattr(self, "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 = self._replace(word)
- new_word = self._replace(new_word)
- 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 = ''
- shlexer.commenters = ''
- argv = list(shlexer)
- return argv
-
- def getargv(self, section, name, default=None, replace=True):
- command = self.getdefault(
- section, name, default=default, replace=False)
- return self._processcommand(command.strip())
-
- def getbool(self, section, name, default=None):
- s = self.getdefault(section, name, default)
+ 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" % (
- section, name))
+ self.section_name, name))
if not isinstance(s, bool):
if s.lower() == "true":
@@ -736,9 +799,16 @@
"boolean value %r needs to be 'True' or 'False'")
return s
- def getdefault(self, section, name, default=None, replace=True):
+ 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):
x = None
- for s in [section] + self.fallbacksections:
+ for s in [self.section_name] + self.fallbacksections:
try:
x = self._cfg[s][name]
break
@@ -751,12 +821,12 @@
x = self._apply_factors(x)
if replace and x and hasattr(x, 'replace'):
- self._subststack.append((section, name))
+ self._subststack.append((self.section_name, name))
try:
x = self._replace(x)
finally:
- assert self._subststack.pop() == (section, name)
- # print "getdefault", section, name, "returned", repr(x)
+ assert self._subststack.pop() == (self.section_name, name)
+ # print "getstring", self.section_name, name, "returned", repr(x)
return x
def _apply_factors(self, s):
@@ -852,8 +922,80 @@
return RE_ITEM_REF.sub(self._replace_match, x)
return x
- def _parse_command(self, command):
- pass
+
+class _ArgvlistReader:
+ @classmethod
+ def getargvlist(cls, reader, section_val):
+ """Parse ``commands`` argvlist multiline string.
+
+ :param str name: Key name in a section.
+ :param str section_val: 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 section_val.splitlines():
+ line = line.rstrip()
+ i = line.find("#")
+ if i != -1:
+ line = line[:i].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)
+ 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_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 = ''
+ shlexer.commenters = ''
+ argv = list(shlexer)
+ return argv
class CommandParser(object):
diff -r 1de999d6ecf98715ce179636a522b86f5dd9b8dc -r 7f1bcc90f673122444ce1efac01206da661c84ff tox/_venv.py
--- a/tox/_venv.py
+++ b/tox/_venv.py
@@ -10,17 +10,17 @@
class CreationConfig:
def __init__(self, md5, python, version, sitepackages,
- develop, deps):
+ usedevelop, deps):
self.md5 = md5
self.python = python
self.version = version
self.sitepackages = sitepackages
- self.develop = develop
+ self.usedevelop = usedevelop
self.deps = deps
def writeconfig(self, path):
lines = ["%s %s" % (self.md5, self.python)]
- lines.append("%s %d %d" % (self.version, self.sitepackages, self.develop))
+ lines.append("%s %d %d" % (self.version, self.sitepackages, self.usedevelop))
for dep in self.deps:
lines.append("%s %s" % dep)
path.ensure()
@@ -32,14 +32,14 @@
lines = path.readlines(cr=0)
value = lines.pop(0).split(None, 1)
md5, python = value
- version, sitepackages, develop = lines.pop(0).split(None, 3)
+ version, sitepackages, usedevelop = lines.pop(0).split(None, 3)
sitepackages = bool(int(sitepackages))
- develop = bool(int(develop))
+ usedevelop = bool(int(usedevelop))
deps = []
for line in lines:
md5, depstring = line.split(None, 1)
deps.append((md5, depstring))
- return CreationConfig(md5, python, version, sitepackages, develop, deps)
+ return CreationConfig(md5, python, version, sitepackages, usedevelop, deps)
except Exception:
return None
@@ -48,7 +48,7 @@
and self.python == other.python
and self.version == other.version
and self.sitepackages == other.sitepackages
- and self.develop == other.develop
+ and self.usedevelop == other.usedevelop
and self.deps == other.deps)
@@ -147,7 +147,7 @@
md5 = getdigest(python)
version = tox.__version__
sitepackages = self.envconfig.sitepackages
- develop = self.envconfig.develop
+ develop = self.envconfig.usedevelop
deps = []
for dep in self._getresolvedeps():
raw_dep = dep.name
@@ -321,11 +321,13 @@
for envname in self.envconfig.passenv:
if envname in os.environ:
env[envname] = os.environ[envname]
- setenv = self.envconfig.setenv
- if setenv:
- env.update(setenv)
+
+ env.update(self.envconfig.setenv)
+
env['VIRTUAL_ENV'] = str(self.path)
+
env.update(extraenv)
+
return env
def test(self, redirect=False):
https://bitbucket.org/hpk42/tox/commits/c2e0dbe2fddd/
Changeset: c2e0dbe2fddd
Branch: pluggy
User: hpk42
Date: 2015-05-11 10:19:03+00:00
Summary: merge default
Affected #: 6 files
diff -r 7f1bcc90f673122444ce1efac01206da661c84ff -r c2e0dbe2fddd4d505b9690071608b819af1a9a08 CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -17,6 +17,12 @@
If platform is set and doesn't match the platform spec in the test
environment the test environment is ignored, no setup or tests are attempted.
+.. (new) add per-venv "ignore_errors" setting, which defaults to False.
+ If ``True``, a non-zero exit code from one command will be ignored and
+ further commands will be executed (which was the default behavior in tox <
+ 2.0). If ``False`` (the default), then a non-zero exit code from one command
+ will abort execution of commands for that environment.
+
- remove the long-deprecated "distribute" option as it has no effect these days.
- fix issue233: avoid hanging with tox-setuptools integration example. Thanks simonb.
diff -r 7f1bcc90f673122444ce1efac01206da661c84ff -r c2e0dbe2fddd4d505b9690071608b819af1a9a08 doc/config.txt
--- a/doc/config.txt
+++ b/doc/config.txt
@@ -110,6 +110,26 @@
pip install {opts} {packages}
+.. confval:: ignore_errors=True|False(default)
+
+ .. versionadded:: 2.0
+
+ If ``True``, a non-zero exit code from one command will be ignored and
+ further commands will be executed (which was the default behavior in tox <
+ 2.0). If ``False`` (the default), then a non-zero exit code from one command
+ will abort execution of commands for that environment.
+
+ It may be helpful to note that this setting is analogous to the ``-i`` or
+ ``ignore-errors`` option of GNU Make. A similar name was chosen to reflect the
+ similarity in function.
+
+ Note that in tox 2.0, the default behavior of tox with respect to
+ treating errors from commands changed. Tox < 2.0 would ignore errors by
+ default. Tox >= 2.0 will abort on an error by default, which is safer and more
+ typical of CI and command execution tools, as it doesn't make sense to
+ run tests if installing some prerequisite failed and it doesn't make sense to
+ try to deploy if tests failed.
+
.. confval:: pip_pre=True|False(default)
.. versionadded:: 1.9
diff -r 7f1bcc90f673122444ce1efac01206da661c84ff -r c2e0dbe2fddd4d505b9690071608b819af1a9a08 tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -589,6 +589,7 @@
assert envconfig.changedir == config.setupdir
assert envconfig.sitepackages is False
assert envconfig.usedevelop is False
+ assert envconfig.ignore_errors is False
assert envconfig.envlogdir == envconfig.envdir.join("log")
assert list(envconfig.setenv.keys()) == ['PYTHONHASHSEED']
hashseed = envconfig.setenv['PYTHONHASHSEED']
@@ -649,6 +650,15 @@
assert envconfig.changedir.basename == "xyz"
assert envconfig.changedir == config.toxinidir.join("xyz")
+ def test_ignore_errors(self, tmpdir, newconfig):
+ config = newconfig("""
+ [testenv]
+ ignore_errors=True
+ """)
+ assert len(config.envconfigs) == 1
+ envconfig = config.envconfigs['python']
+ assert envconfig.ignore_errors is True
+
def test_envbindir(self, tmpdir, newconfig):
config = newconfig("""
[testenv]
diff -r 7f1bcc90f673122444ce1efac01206da661c84ff -r c2e0dbe2fddd4d505b9690071608b819af1a9a08 tox/_cmdline.py
--- a/tox/_cmdline.py
+++ b/tox/_cmdline.py
@@ -179,7 +179,7 @@
raise tox.exception.InvocationError(
"%s (see %s)" % (invoked, outpath), ret)
else:
- raise tox.exception.InvocationError("%r" % (invoked, ))
+ raise tox.exception.InvocationError("%r" % (invoked, ), ret)
if not out and outpath:
out = outpath.read()
if hasattr(self, "commandlog"):
diff -r 7f1bcc90f673122444ce1efac01206da661c84ff -r c2e0dbe2fddd4d505b9690071608b819af1a9a08 tox/_config.py
--- a/tox/_config.py
+++ b/tox/_config.py
@@ -335,6 +335,11 @@
"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(config, reader, section_val):
if config.option.recreate:
return True
@@ -644,15 +649,6 @@
if env_attr.name == "install_command":
reader.addsubstitutions(envbindir=vc.envbindir, envpython=vc.envpython,
envsitepackagesdir=vc.envsitepackagesdir)
-
- # XXX introduce some testenv verification like this:
- # try:
- # sec = self._cfg[section]
- # except KeyError:
- # sec = self._cfg["testenv"]
- # for name in sec:
- # if name not in names:
- # print ("unknown testenv attribute: %r" % (name,))
return vc
def _getenvdata(self, reader):
diff -r 7f1bcc90f673122444ce1efac01206da661c84ff -r c2e0dbe2fddd4d505b9690071608b819af1a9a08 tox/_venv.py
--- a/tox/_venv.py
+++ b/tox/_venv.py
@@ -363,6 +363,14 @@
val = sys.exc_info()[1]
self.session.report.error(str(val))
self.status = "commands failed"
+ if not self.envconfig.ignore_errors:
+ self.session.report.error(
+ 'Stopping processing of commands for env %s '
+ 'because `%s` failed with exit code %s'
+ % (self.name,
+ ' '.join([str(x) for x in argv]),
+ val.args[1]))
+ break # Don't process remaining commands
except KeyboardInterrupt:
self.status = "keyboardinterrupt"
self.session.report.error(self.status)
https://bitbucket.org/hpk42/tox/commits/d7d35b623979/
Changeset: d7d35b623979
User: hpk42
Date: 2015-05-11 10:35:22+00:00
Summary: remove erroring code / somewhat superflous error reporting
Affected #: 1 file
diff -r 3d7925f2ef6c716de3bc3e84f649ce9b5f15b498 -r d7d35b623979d2e8186798c5bd29a679ef825c56 tox/_venv.py
--- a/tox/_venv.py
+++ b/tox/_venv.py
@@ -357,17 +357,10 @@
try:
self._pcall(argv, cwd=cwd, action=action, redirect=redirect,
ignore_ret=ignore_ret)
- except tox.exception.InvocationError:
- val = sys.exc_info()[1]
- self.session.report.error(str(val))
+ except tox.exception.InvocationError as err:
+ self.session.report.error(str(err))
self.status = "commands failed"
if not self.envconfig.ignore_errors:
- self.session.report.error(
- 'Stopping processing of commands for env %s '
- 'because `%s` failed with exit code %s'
- % (self.name,
- ' '.join([str(x) for x in argv]),
- val.args[1]))
break # Don't process remaining commands
except KeyboardInterrupt:
self.status = "keyboardinterrupt"
https://bitbucket.org/hpk42/tox/commits/06bb44d51a4c/
Changeset: 06bb44d51a4c
User: hpk42
Date: 2015-05-11 10:35:56+00:00
Summary: merge pluggy branch
Affected #: 15 files
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -38,6 +38,13 @@
- fix issue240: allow to specify empty argument list without it being
rewritten to ".". Thanks Daniel Hahler.
+- introduce experimental (not much documented yet) plugin system
+ based on pytest's externalized "pluggy" system.
+ See tox/hookspecs.py for the current hooks.
+
+- introduce parser.add_testenv_attribute() to register an ini-variable
+ for testenv sections. Can be used from plugins through the
+ tox_add_option hook.
1.9.2
-----------
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c doc/conf.py
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -48,8 +48,8 @@
# built documents.
#
# The short X.Y version.
-release = "1.9"
-version = "1.9.0"
+release = "2.0"
+version = "2.0.0"
# The full version, including alpha/beta/rc tags.
# The language for content autogenerated by Sphinx. Refer to documentation
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c doc/index.txt
--- a/doc/index.txt
+++ b/doc/index.txt
@@ -5,7 +5,7 @@
---------------------------------------------
``tox`` aims to automate and standardize testing in Python. It is part
-of a larger vision of easing the packaging, testing and release process
+of a larger vision of easing the packaging, testing and release process
of Python software.
What is Tox?
@@ -21,6 +21,7 @@
* acting as a frontend to Continuous Integration servers, greatly
reducing boilerplate and merging CI and shell-based testing.
+
Basic example
-----------------
@@ -62,10 +63,10 @@
- test-tool agnostic: runs py.test, nose or unittests in a uniform manner
-* supports :ref:`using different / multiple PyPI index servers <multiindex>`
+* :doc:`(new in 2.0) plugin system <plugins>` to modify tox execution with simple hooks.
* uses pip_ and setuptools_ by default. Experimental
- support for configuring the installer command
+ support for configuring the installer command
through :confval:`install_command=ARGV`.
* **cross-Python compatible**: CPython-2.6, 2.7, 3.2 and higher,
@@ -74,11 +75,11 @@
* **cross-platform**: Windows and Unix style environments
* **integrates with continuous integration servers** like Jenkins_
- (formerly known as Hudson) and helps you to avoid boilerplatish
+ (formerly known as Hudson) and helps you to avoid boilerplatish
and platform-specific build-step hacks.
* **full interoperability with devpi**: is integrated with and
- is used for testing in the devpi_ system, a versatile pypi
+ is used for testing in the devpi_ system, a versatile pypi
index server and release managing tool.
* **driven by a simple ini-style config file**
@@ -89,6 +90,9 @@
* **professionally** :doc:`supported <support>`
+* supports :ref:`using different / multiple PyPI index servers <multiindex>`
+
+
.. _pypy: http://pypy.org
.. _`tox.ini`: :doc:configfile
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c doc/plugins.txt
--- /dev/null
+++ b/doc/plugins.txt
@@ -0,0 +1,69 @@
+.. be in -*- rst -*- mode!
+
+tox plugins
+===========
+
+.. versionadded:: 2.0
+
+With tox-2.0 a few aspects of tox running can be experimentally modified
+by writing hook functions. We expect the list of hook function to grow
+over time.
+
+writing a setuptools entrypoints plugin
+---------------------------------------
+
+If you have a ``tox_MYPLUGIN.py`` module you could use the following
+rough ``setup.py`` to make it into a package which you can upload to the
+Python packaging index::
+
+ # content of setup.py
+ from setuptools import setup
+
+ if __name__ == "__main__":
+ setup(
+ name='tox-MYPLUGIN',
+ description='tox plugin decsription',
+ license="MIT license",
+ version='0.1',
+ py_modules=['tox_MYPLUGIN'],
+ entry_points={'tox': ['MYPLUGIN = tox_MYPLUGIN']},
+ install_requires=['tox>=2.0'],
+ )
+
+You can then install the plugin to develop it via::
+
+ pip install -e .
+
+and later publish it.
+
+The ``entry_points`` part allows tox to see your plugin during startup.
+
+
+Writing hook implementations
+----------------------------
+
+A plugin module needs can define one or more hook implementation functions::
+
+ from tox import hookimpl
+
+ @hookimpl
+ def tox_addoption(parser):
+ # add your own command line options
+
+
+ @hookimpl
+ def tox_configure(config):
+ # post process tox configuration after cmdline/ini file have
+ # been parsed
+
+If you put this into a module and make it pypi-installable with the ``tox``
+entry point you'll get your code executed as part of a tox run.
+
+
+
+tox hook specifications
+----------------------------
+
+.. automodule:: tox.hookspecs
+ :members:
+
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c setup.py
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@
def main():
version = sys.version_info[:2]
- install_requires = ['virtualenv>=1.11.2', 'py>=1.4.17', ]
+ install_requires = ['virtualenv>=1.11.2', 'py>=1.4.17', 'pluggy>=0.3.0,<0.4.0']
if version < (2, 7):
install_requires += ['argparse']
setup(
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -6,7 +6,6 @@
import tox
import tox._config
from tox._config import * # noqa
-from tox._config import _split_env
from tox._venv import VirtualEnv
@@ -19,7 +18,7 @@
assert config.toxworkdir.realpath() == tmpdir.join(".tox").realpath()
assert config.envconfigs['py1'].basepython == sys.executable
assert config.envconfigs['py1'].deps == []
- assert not config.envconfigs['py1'].platform
+ assert config.envconfigs['py1'].platform == ".*"
def test_config_parsing_multienv(self, tmpdir, newconfig):
config = newconfig([], """
@@ -93,12 +92,12 @@
"""
Ensure correct parseini._is_same_dep is working with a few samples.
"""
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>=2.0')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>2.0')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<2.0')
- assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<=2.0')
- assert not parseini._is_same_dep('pkg_hello-world3==1.0', 'otherpkg>=2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>=2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3>2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<2.0')
+ assert DepOption._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<=2.0')
+ assert not DepOption._is_same_dep('pkg_hello-world3==1.0', 'otherpkg>=2.0')
class TestConfigPlatform:
@@ -220,8 +219,8 @@
commands =
echo {[section]key}
""")
- reader = IniReader(config._cfg)
- x = reader.getargvlist("testenv", "commands")
+ reader = SectionReader("testenv", config._cfg)
+ x = reader.getargvlist("commands")
assert x == [["echo", "whatever"]]
def test_command_substitution_from_other_section_multiline(self, newconfig):
@@ -245,8 +244,8 @@
# comment is omitted
echo {[base]commands}
""")
- reader = IniReader(config._cfg)
- x = reader.getargvlist("testenv", "commands")
+ reader = SectionReader("testenv", config._cfg)
+ x = reader.getargvlist("commands")
assert x == [
"cmd1 param11 param12".split(),
"cmd2 param21 param22".split(),
@@ -257,16 +256,16 @@
class TestIniParser:
- def test_getdefault_single(self, tmpdir, newconfig):
+ def test_getstring_single(self, tmpdir, newconfig):
config = newconfig("""
[section]
key=value
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("section", "key")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getstring("key")
assert x == "value"
- assert not reader.getdefault("section", "hello")
- x = reader.getdefault("section", "hello", "world")
+ assert not reader.getstring("hello")
+ x = reader.getstring("hello", "world")
assert x == "world"
def test_missing_substitution(self, tmpdir, newconfig):
@@ -274,40 +273,40 @@
[mydefault]
key2={xyz}
""")
- reader = IniReader(config._cfg, fallbacksections=['mydefault'])
+ reader = SectionReader("mydefault", config._cfg, fallbacksections=['mydefault'])
assert reader is not None
- py.test.raises(tox.exception.ConfigError,
- 'reader.getdefault("mydefault", "key2")')
+ with py.test.raises(tox.exception.ConfigError):
+ reader.getstring("key2")
- def test_getdefault_fallback_sections(self, tmpdir, newconfig):
+ def test_getstring_fallback_sections(self, tmpdir, newconfig):
config = newconfig("""
[mydefault]
key2=value2
[section]
key=value
""")
- reader = IniReader(config._cfg, fallbacksections=['mydefault'])
- x = reader.getdefault("section", "key2")
+ reader = SectionReader("section", config._cfg, fallbacksections=['mydefault'])
+ x = reader.getstring("key2")
assert x == "value2"
- x = reader.getdefault("section", "key3")
+ x = reader.getstring("key3")
assert not x
- x = reader.getdefault("section", "key3", "world")
+ x = reader.getstring("key3", "world")
assert x == "world"
- def test_getdefault_substitution(self, tmpdir, newconfig):
+ def test_getstring_substitution(self, tmpdir, newconfig):
config = newconfig("""
[mydefault]
key2={value2}
[section]
key={value}
""")
- reader = IniReader(config._cfg, fallbacksections=['mydefault'])
+ reader = SectionReader("section", config._cfg, fallbacksections=['mydefault'])
reader.addsubstitutions(value="newvalue", value2="newvalue2")
- x = reader.getdefault("section", "key2")
+ x = reader.getstring("key2")
assert x == "newvalue2"
- x = reader.getdefault("section", "key3")
+ x = reader.getstring("key3")
assert not x
- x = reader.getdefault("section", "key3", "{value2}")
+ x = reader.getstring("key3", "{value2}")
assert x == "newvalue2"
def test_getlist(self, tmpdir, newconfig):
@@ -317,9 +316,9 @@
item1
{item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="not", item2="grr")
- x = reader.getlist("section", "key2")
+ x = reader.getlist("key2")
assert x == ['item1', 'grr']
def test_getdict(self, tmpdir, newconfig):
@@ -329,28 +328,31 @@
key1=item1
key2={item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="not", item2="grr")
- x = reader.getdict("section", "key2")
+ x = reader.getdict("key2")
assert 'key1' in x
assert 'key2' in x
assert x['key1'] == 'item1'
assert x['key2'] == 'grr'
- def test_getdefault_environment_substitution(self, monkeypatch, newconfig):
+ x = reader.getdict("key3", {1: 2})
+ assert x == {1: 2}
+
+ def test_getstring_environment_substitution(self, monkeypatch, newconfig):
monkeypatch.setenv("KEY1", "hello")
config = newconfig("""
[section]
key1={env:KEY1}
key2={env:KEY2}
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("section", "key1")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getstring("key1")
assert x == "hello"
- py.test.raises(tox.exception.ConfigError,
- 'reader.getdefault("section", "key2")')
+ with py.test.raises(tox.exception.ConfigError):
+ reader.getstring("key2")
- def test_getdefault_environment_substitution_with_default(self, monkeypatch, newconfig):
+ def test_getstring_environment_substitution_with_default(self, monkeypatch, newconfig):
monkeypatch.setenv("KEY1", "hello")
config = newconfig("""
[section]
@@ -358,12 +360,12 @@
key2={env:KEY2:DEFAULT_VALUE}
key3={env:KEY3:}
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("section", "key1")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getstring("key1")
assert x == "hello"
- x = reader.getdefault("section", "key2")
+ x = reader.getstring("key2")
assert x == "DEFAULT_VALUE"
- x = reader.getdefault("section", "key3")
+ x = reader.getstring("key3")
assert x == ""
def test_value_matches_section_substituion(self):
@@ -374,15 +376,15 @@
assert is_section_substitution("{[setup]}") is None
assert is_section_substitution("{[setup] commands}") is None
- def test_getdefault_other_section_substitution(self, newconfig):
+ def test_getstring_other_section_substitution(self, newconfig):
config = newconfig("""
[section]
key = rue
[testenv]
key = t{[section]key}
""")
- reader = IniReader(config._cfg)
- x = reader.getdefault("testenv", "key")
+ reader = SectionReader("testenv", config._cfg)
+ x = reader.getstring("key")
assert x == "true"
def test_argvlist(self, tmpdir, newconfig):
@@ -392,12 +394,12 @@
cmd1 {item1} {item2}
cmd2 {item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="with space", item2="grr")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "with", "space", "grr"],
["cmd2", "grr"]]
@@ -406,9 +408,9 @@
[section]
comm = py.test {posargs}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions([r"hello\this"])
- argv = reader.getargv("section", "comm")
+ argv = reader.getargv("comm")
assert argv == ["py.test", "hello\\this"]
def test_argvlist_multiline(self, tmpdir, newconfig):
@@ -418,12 +420,12 @@
cmd1 {item1} \ # a comment
{item2}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(item1="with space", item2="grr")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "with", "space", "grr"]]
def test_argvlist_quoting_in_command(self, tmpdir, newconfig):
@@ -433,8 +435,8 @@
cmd1 'with space' \ # a comment
'after the comment'
""")
- reader = IniReader(config._cfg)
- x = reader.getargvlist("section", "key1")
+ reader = SectionReader("section", config._cfg)
+ x = reader.getargvlist("key1")
assert x == [["cmd1", "with space", "after the comment"]]
def test_argvlist_positional_substitution(self, tmpdir, newconfig):
@@ -445,22 +447,22 @@
cmd2 {posargs:{item2} \
other}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
posargs = ['hello', 'world']
reader.addsubstitutions(posargs, item2="value2")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- argvlist = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ argvlist = reader.getargvlist("key2")
assert argvlist[0] == ["cmd1"] + posargs
assert argvlist[1] == ["cmd2"] + posargs
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions([], item2="value2")
# py.test.raises(tox.exception.ConfigError,
- # "reader.getargvlist('section', 'key1')")
- assert reader.getargvlist('section', 'key1') == []
- argvlist = reader.getargvlist("section", "key2")
+ # "reader.getargvlist('key1')")
+ assert reader.getargvlist('key1') == []
+ argvlist = reader.getargvlist("key2")
assert argvlist[0] == ["cmd1"]
assert argvlist[1] == ["cmd2", "value2", "other"]
@@ -472,10 +474,10 @@
cmd2 -f '{posargs}'
cmd3 -f {posargs}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(["foo", "bar"])
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "--foo-args=foo bar"],
["cmd2", "-f", "foo bar"],
["cmd3", "-f", "foo", "bar"]]
@@ -486,10 +488,10 @@
key2=
cmd1 -f {posargs}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(["foo", "'bar", "baz'"])
- assert reader.getargvlist('section', 'key1') == []
- x = reader.getargvlist("section", "key2")
+ assert reader.getargvlist('key1') == []
+ x = reader.getargvlist("key2")
assert x == [["cmd1", "-f", "foo", "bar baz"]]
def test_positional_arguments_are_only_replaced_when_standing_alone(self, tmpdir, newconfig):
@@ -501,11 +503,11 @@
cmd2 -m '\'something\'' []
cmd3 something[]else
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
posargs = ['hello', 'world']
reader.addsubstitutions(posargs)
- argvlist = reader.getargvlist('section', 'key')
+ argvlist = reader.getargvlist('key')
assert argvlist[0] == ['cmd0'] + posargs
assert argvlist[1] == ['cmd1', '-m', '[abc]']
assert argvlist[2] == ['cmd2', '-m', "something"] + posargs
@@ -517,32 +519,32 @@
key = py.test -n5 --junitxml={envlogdir}/junit-{envname}.xml []
"""
config = newconfig(inisource)
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
posargs = ['hello', 'world']
reader.addsubstitutions(posargs, envlogdir='ENV_LOG_DIR', envname='ENV_NAME')
expected = [
'py.test', '-n5', '--junitxml=ENV_LOG_DIR/junit-ENV_NAME.xml', 'hello', 'world'
]
- assert reader.getargvlist('section', 'key')[0] == expected
+ assert reader.getargvlist('key')[0] == expected
def test_getargv(self, newconfig):
config = newconfig("""
[section]
key=some command "with quoting"
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
expected = ['some', 'command', 'with quoting']
- assert reader.getargv('section', 'key') == expected
+ assert reader.getargv('key') == expected
def test_getpath(self, tmpdir, newconfig):
config = newconfig("""
[section]
path1={HELLO}
""")
- reader = IniReader(config._cfg)
+ reader = SectionReader("section", config._cfg)
reader.addsubstitutions(toxinidir=tmpdir, HELLO="mypath")
- x = reader.getpath("section", "path1", tmpdir)
+ x = reader.getpath("path1", tmpdir)
assert x == tmpdir.join("mypath")
def test_getbool(self, tmpdir, newconfig):
@@ -554,13 +556,13 @@
key2a=falsE
key5=yes
""")
- reader = IniReader(config._cfg)
- assert reader.getbool("section", "key1") is True
- assert reader.getbool("section", "key1a") is True
- assert reader.getbool("section", "key2") is False
- assert reader.getbool("section", "key2a") is False
- py.test.raises(KeyError, 'reader.getbool("section", "key3")')
- py.test.raises(tox.exception.ConfigError, 'reader.getbool("section", "key5")')
+ reader = SectionReader("section", config._cfg)
+ assert reader.getbool("key1") is True
+ assert reader.getbool("key1a") is True
+ assert reader.getbool("key2") is False
+ assert reader.getbool("key2a") is False
+ py.test.raises(KeyError, 'reader.getbool("key3")')
+ py.test.raises(tox.exception.ConfigError, 'reader.getbool("key5")')
class TestConfigTestEnv:
@@ -586,7 +588,7 @@
assert envconfig.commands == [["xyz", "--abc"]]
assert envconfig.changedir == config.setupdir
assert envconfig.sitepackages is False
- assert envconfig.develop is False
+ assert envconfig.usedevelop is False
assert envconfig.ignore_errors is False
assert envconfig.envlogdir == envconfig.envdir.join("log")
assert list(envconfig.setenv.keys()) == ['PYTHONHASHSEED']
@@ -607,7 +609,7 @@
[testenv]
usedevelop = True
""")
- assert not config.envconfigs["python"].develop
+ assert not config.envconfigs["python"].usedevelop
def test_specific_command_overrides(self, tmpdir, newconfig):
config = newconfig("""
@@ -1382,7 +1384,7 @@
def test_noset(self, tmpdir, newconfig):
args = ['--hashseed', 'noset']
envconfig = self._get_envconfig(newconfig, args=args)
- assert envconfig.setenv is None
+ assert envconfig.setenv == {}
def test_noset_with_setenv(self, tmpdir, newconfig):
tox_ini = """
@@ -1571,31 +1573,16 @@
])
-class TestArgumentParser:
-
- def test_dash_e_single_1(self):
- parser = prepare_parse('testpkg')
- args = parser.parse_args('-e py26'.split())
- envlist = _split_env(args.env)
- assert envlist == ['py26']
-
- def test_dash_e_single_2(self):
- parser = prepare_parse('testpkg')
- args = parser.parse_args('-e py26,py33'.split())
- envlist = _split_env(args.env)
- assert envlist == ['py26', 'py33']
-
- def test_dash_e_same(self):
- parser = prepare_parse('testpkg')
- args = parser.parse_args('-e py26,py26'.split())
- envlist = _split_env(args.env)
- assert envlist == ['py26', 'py26']
-
- def test_dash_e_combine(self):
- parser = prepare_parse('testpkg')
- args = parser.parse_args('-e py26,py25,py33 -e py33,py27'.split())
- envlist = _split_env(args.env)
- assert envlist == ['py26', 'py25', 'py33', 'py33', 'py27']
+ at pytest.mark.parametrize("cmdline,envlist", [
+ ("-e py26", ['py26']),
+ ("-e py26,py33", ['py26', 'py33']),
+ ("-e py26,py26", ['py26', 'py26']),
+ ("-e py26,py33 -e py33,py27", ['py26', 'py33', 'py33', 'py27'])
+])
+def test_env_spec(cmdline, envlist):
+ args = cmdline.split()
+ config = parseconfig(args)
+ assert config.envlist == envlist
class TestCommandParser:
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tests/test_interpreters.py
--- a/tests/test_interpreters.py
+++ b/tests/test_interpreters.py
@@ -3,11 +3,13 @@
import pytest
from tox.interpreters import * # noqa
+from tox._config import get_plugin_manager
@pytest.fixture
def interpreters():
- return Interpreters()
+ pm = get_plugin_manager()
+ return Interpreters(hook=pm.hook)
@pytest.mark.skipif("sys.platform != 'win32'")
@@ -28,8 +30,11 @@
assert locate_via_py('3', '2') == sys.executable
-def test_find_executable():
- p = find_executable(sys.executable)
+def test_tox_get_python_executable():
+ class envconfig:
+ basepython = sys.executable
+ envname = "pyxx"
+ p = tox_get_python_executable(envconfig)
assert p == py.path.local(sys.executable)
for ver in [""] + "2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3".split():
name = "python%s" % ver
@@ -42,7 +47,8 @@
else:
if not py.path.local.sysfind(name):
continue
- p = find_executable(name)
+ envconfig.basepython = name
+ p = tox_get_python_executable(envconfig)
assert p
popen = py.std.subprocess.Popen([str(p), '-V'],
stderr=py.std.subprocess.PIPE)
@@ -55,7 +61,12 @@
def sysfind(x):
return "hello"
monkeypatch.setattr(py.path.local, "sysfind", sysfind)
- t = find_executable("qweqwe")
+
+ class envconfig:
+ basepython = "1lk23j"
+ envname = "pyxx"
+
+ t = tox_get_python_executable(envconfig)
assert t == "hello"
@@ -69,31 +80,33 @@
class TestInterpreters:
- def test_get_info_self_exceptions(self, interpreters):
- pytest.raises(ValueError, lambda:
- interpreters.get_info())
- pytest.raises(ValueError, lambda:
- interpreters.get_info(name="12", executable="123"))
+ def test_get_executable(self, interpreters):
+ class envconfig:
+ basepython = sys.executable
+ envname = "pyxx"
- def test_get_executable(self, interpreters):
- x = interpreters.get_executable(sys.executable)
+ x = interpreters.get_executable(envconfig)
assert x == sys.executable
- assert not interpreters.get_executable("12l3k1j23")
-
- def test_get_info__name(self, interpreters):
- info = interpreters.get_info(executable=sys.executable)
+ info = interpreters.get_info(envconfig)
assert info.version_info == tuple(sys.version_info)
assert info.executable == sys.executable
assert info.runnable
- def test_get_info__name_not_exists(self, interpreters):
- info = interpreters.get_info("qlwkejqwe")
+ def test_get_executable_no_exist(self, interpreters):
+ class envconfig:
+ basepython = "1lkj23"
+ envname = "pyxx"
+ assert not interpreters.get_executable(envconfig)
+ info = interpreters.get_info(envconfig)
assert not info.version_info
- assert info.name == "qlwkejqwe"
+ assert info.name == "1lkj23"
assert not info.executable
assert not info.runnable
def test_get_sitepackagesdir_error(self, interpreters):
- info = interpreters.get_info(sys.executable)
+ class envconfig:
+ basepython = sys.executable
+ envname = "123"
+ info = interpreters.get_info(envconfig)
s = interpreters.get_sitepackagesdir(info, "")
assert s
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tests/test_venv.py
--- a/tests/test_venv.py
+++ b/tests/test_venv.py
@@ -35,6 +35,7 @@
py.test.raises(tox.exception.UnsupportedInterpreter,
venv.getsupportedinterpreter)
monkeypatch.undo()
+ monkeypatch.setattr(venv.envconfig, "envname", "py1")
monkeypatch.setattr(venv.envconfig, 'basepython', 'notexistingpython')
py.test.raises(tox.exception.InterpreterNotFound,
venv.getsupportedinterpreter)
@@ -42,7 +43,7 @@
# check that we properly report when no version_info is present
info = NoInterpreterInfo(name=venv.name)
info.executable = "something"
- monkeypatch.setattr(config.interpreters, "get_info", lambda *args: info)
+ monkeypatch.setattr(config.interpreters, "get_info", lambda *args, **kw: info)
pytest.raises(tox.exception.InvocationError, venv.getsupportedinterpreter)
@@ -65,7 +66,7 @@
# assert Envconfig.toxworkdir in args
assert venv.getcommandpath("easy_install", cwd=py.path.local())
interp = venv._getliveconfig().python
- assert interp == venv.envconfig._basepython_info.executable
+ assert interp == venv.envconfig.python_info.executable
assert venv.path_config.check(exists=False)
@@ -463,7 +464,7 @@
venv = VirtualEnv(envconfig, session=mocksession)
venv.update()
cconfig = venv._getliveconfig()
- cconfig.develop = True
+ cconfig.usedevelop = True
cconfig.writeconfig(venv.path_config)
mocksession._clearmocks()
venv.update()
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tox.ini
--- a/tox.ini
+++ b/tox.ini
@@ -5,8 +5,10 @@
commands=echo {posargs}
[testenv]
-commands=py.test --junitxml={envlogdir}/junit-{envname}.xml {posargs}
+commands= py.test --timeout=60 {posargs}
+
deps=pytest>=2.3.5
+ pytest-timeout
[testenv:docs]
basepython=python
@@ -14,15 +16,16 @@
deps=sphinx
{[testenv]deps}
commands=
- py.test -v \
- --junitxml={envlogdir}/junit-{envname}.xml \
- check_sphinx.py {posargs}
+ py.test -v check_sphinx.py {posargs}
[testenv:flakes]
+qwe = 123
deps = pytest-flakes>=0.2
pytest-pep8
-commands = py.test -x --flakes --pep8 tox tests
+commands =
+ py.test --flakes -m flakes tox tests
+ py.test --pep8 -m pep8 tox tests
[testenv:dev]
# required to make looponfail reload on every source code change
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tox/__init__.py
--- a/tox/__init__.py
+++ b/tox/__init__.py
@@ -1,6 +1,8 @@
#
__version__ = '2.0.0.dev1'
+from .hookspecs import hookspec, hookimpl # noqa
+
class exception:
class Error(Exception):
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tox/_cmdline.py
--- a/tox/_cmdline.py
+++ b/tox/_cmdline.py
@@ -24,13 +24,35 @@
def main(args=None):
try:
- config = parseconfig(args, 'tox')
+ config = parseconfig(args)
+ if config.option.help:
+ show_help(config)
+ raise SystemExit(0)
+ elif config.option.helpini:
+ show_help_ini(config)
+ raise SystemExit(0)
retcode = Session(config).runcommand()
raise SystemExit(retcode)
except KeyboardInterrupt:
raise SystemExit(2)
+def show_help(config):
+ tw = py.io.TerminalWriter()
+ tw.write(config._parser.format_help())
+ tw.line()
+
+
+def show_help_ini(config):
+ tw = py.io.TerminalWriter()
+ tw.sep("-", "per-testenv attributes")
+ for env_attr in config._testenv_attr:
+ tw.line("%-15s %-8s default: %s" %
+ (env_attr.name, "<" + env_attr.type + ">", env_attr.default), bold=True)
+ tw.line(env_attr.help)
+ tw.line()
+
+
class Action(object):
def __init__(self, session, venv, msg, args):
self.venv = venv
@@ -487,7 +509,7 @@
venv.status = "platform mismatch"
continue # we simply omit non-matching platforms
if self.setupenv(venv):
- if venv.envconfig.develop:
+ if venv.envconfig.usedevelop:
self.developpkg(venv, self.config.setupdir)
elif self.config.skipsdist or venv.envconfig.skip_install:
self.finishvenv(venv)
@@ -551,8 +573,7 @@
for envconfig in self.config.envconfigs.values():
self.report.line("[testenv:%s]" % envconfig.envname, bold=True)
self.report.line(" basepython=%s" % envconfig.basepython)
- self.report.line(" _basepython_info=%s" %
- envconfig._basepython_info)
+ self.report.line(" pythoninfo=%s" % (envconfig.python_info,))
self.report.line(" envpython=%s" % envconfig.envpython)
self.report.line(" envtmpdir=%s" % envconfig.envtmpdir)
self.report.line(" envbindir=%s" % envconfig.envbindir)
@@ -567,7 +588,7 @@
self.report.line(" deps=%s" % envconfig.deps)
self.report.line(" envdir= %s" % envconfig.envdir)
self.report.line(" downloadcache=%s" % envconfig.downloadcache)
- self.report.line(" usedevelop=%s" % envconfig.develop)
+ self.report.line(" usedevelop=%s" % envconfig.usedevelop)
def showenvs(self):
for env in self.config.envlist:
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tox/_config.py
--- a/tox/_config.py
+++ b/tox/_config.py
@@ -8,8 +8,10 @@
import string
import pkg_resources
import itertools
+import pluggy
-from tox.interpreters import Interpreters
+import tox.interpreters
+from tox import hookspecs
import py
@@ -22,20 +24,161 @@
for version in '24,25,26,27,30,31,32,33,34,35'.split(','):
default_factors['py' + version] = 'python%s.%s' % tuple(version)
+hookimpl = pluggy.HookimplMarker("tox")
-def parseconfig(args=None, pkg=None):
+
+def get_plugin_manager():
+ # initialize plugin manager
+ pm = pluggy.PluginManager("tox")
+ pm.add_hookspecs(hookspecs)
+ pm.register(tox._config)
+ pm.register(tox.interpreters)
+ pm.load_setuptools_entrypoints("tox")
+ pm.check_pending()
+ return pm
+
+
+class MyParser:
+ def __init__(self):
+ self.argparser = argparse.ArgumentParser(
+ description="tox options", add_help=False)
+ self._testenv_attr = []
+
+ def add_argument(self, *args, **kwargs):
+ return self.argparser.add_argument(*args, **kwargs)
+
+ def add_testenv_attribute(self, name, type, help, default=None, postprocess=None):
+ self._testenv_attr.append(VenvAttribute(name, type, default, help, postprocess))
+
+ def add_testenv_attribute_obj(self, obj):
+ 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, config, reader, section_val):
+ deps = []
+ for depline in section_val:
+ 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
+ dep2_name = pkg_resources.Requirement.parse(dep2).project_name
+ 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, config, reader, section_val):
+ args = config.option.args
+ if args:
+ if section_val:
+ args = []
+ for arg in config.option.args:
+ if arg:
+ origpath = config.invocationcwd.join(arg, abs=True)
+ if origpath.check():
+ arg = reader.getpath("changedir", ".").bestrelpath(origpath)
+ args.append(arg)
+ reader.addsubstitutions(args)
+ return section_val
+
+
+class InstallcmdOption:
+ name = "install_command"
+ type = "argv"
+ default = "pip install {opts} {packages}"
+ help = "install command for dependencies and package under test."
+
+ def postprocess(self, config, reader, section_val):
+ if '{packages}' not in section_val:
+ raise tox.exception.ConfigError(
+ "'install_command' must contain '{packages}' substitution")
+ return section_val
+
+
+def parseconfig(args=None):
"""
: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()
+
if args is None:
args = sys.argv[1:]
- parser = prepare_parse(pkg)
- opts = parser.parse_args(args)
- config = Config()
- config.option = opts
+
+ # prepare command line options
+ parser = MyParser()
+ 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)
@@ -52,6 +195,10 @@
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
@@ -63,10 +210,8 @@
class VersionAction(argparse.Action):
def __call__(self, argparser, *args, **kwargs):
- name = argparser.pkgname
- mod = __import__(name)
- version = mod.__version__
- py.builtin.print_("%s imported from %s" % (version, mod.__file__))
+ version = tox.__version__
+ py.builtin.print_("%s imported from %s" % (version, tox.__file__))
raise SystemExit(0)
@@ -78,13 +223,16 @@
setattr(namespace, self.dest, 0)
-def prepare_parse(pkgname):
- parser = argparse.ArgumentParser(description=__doc__,)
+ at hookimpl
+def tox_addoption(parser):
# formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.pkgname = pkgname
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.")
@@ -130,6 +278,7 @@
"all commands and results involved. This will turn off "
"pass-through output from running test commands which is "
"instead captured into the json result file.")
+
# We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED.
parser.add_argument("--hashseed", action="store",
metavar="SEED", default=None,
@@ -149,14 +298,144 @@
parser.add_argument("args", nargs="*",
help="additional arguments available to command positional substitution")
- return parser
+
+ # add various core venv interpreter attributes
+
+ parser.add_testenv_attribute(
+ name="envdir", type="path", default="{toxworkdir}/{envname}",
+ help="venv directory")
+
+ 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(config, reader, section_val):
+ if section_val:
+ # env var, if present, takes precedence
+ downloadcache = os.environ.get("PIP_DOWNLOAD_CACHE", section_val)
+ 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(config, reader, section_val):
+ if config.option.recreate:
+ return True
+ return section_val
+
+ parser.add_testenv_attribute(
+ name="recreate", type="bool", default=False, postprocess=recreate,
+ help="always recreate this test environment.")
+
+ def setenv(config, reader, section_val):
+ setenv = section_val
+ 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", postprocess=setenv,
+ help="list of X=Y lines with environment variable settings")
+
+ def passenv(config, reader, section_val):
+ passenv = set(["PATH"])
+ if sys.platform == "win32":
+ passenv.add("SYSTEMROOT") # needed for python's crypto module
+ passenv.add("PATHEXT") # needed for discovering executables
+ for spec in section_val:
+ for name in os.environ:
+ if fnmatchcase(name.upper(), spec.upper()):
+ passenv.add(name)
+ return passenv
+
+ parser.add_testenv_attribute(
+ name="passenv", type="space-separated-list", postprocess=passenv,
+ help="environment variables names which shall be passed "
+ "from tox invocation to test environment when executing commands.")
+
+ 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(config, reader, section_val):
+ return config.option.sitepackages or section_val
+
+ 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(config, reader, section_val):
+ return config.option.pre or section_val
+
+ 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(config, reader, section_val):
+ return not config.option.installpkg and (section_val or config.option.develop)
+
+ parser.add_testenv_attribute(
+ name="usedevelop", type="bool", postprocess=develop, default=False,
+ help="install package in develop/editable mode")
+
+ def basepython_default(config, reader, section_val):
+ if section_val is None:
+ for f in reader.factors:
+ if f in default_factors:
+ return default_factors[f]
+ return sys.executable
+ return str(section_val)
+
+ 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_obj(InstallcmdOption())
+ 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.")
class Config(object):
- def __init__(self):
+ def __init__(self, pluginmanager, option, interpreters):
self.envconfigs = {}
self.invocationcwd = py.path.local()
- self.interpreters = Interpreters()
+ self.interpreters = interpreters
+ self.pluginmanager = pluginmanager
+ self.option = option
@property
def homedir(self):
@@ -192,16 +471,20 @@
def envsitepackagesdir(self):
self.getsupportedinterpreter() # for throwing exceptions
x = self.config.interpreters.get_sitepackagesdir(
- info=self._basepython_info,
+ info=self.python_info,
envdir=self.envdir)
return x
+ @property
+ def python_info(self):
+ 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(self.basepython)
+ info = self.config.interpreters.get_info(envconfig=self)
if not info.executable:
raise tox.exception.InterpreterNotFound(self.basepython)
if not info.version_info:
@@ -240,12 +523,10 @@
self.config = config
ctxname = getcontextname()
if ctxname == "jenkins":
- reader = IniReader(self._cfg, fallbacksections=['tox'])
- toxsection = "tox:%s" % ctxname
+ reader = SectionReader("tox:jenkins", self._cfg, fallbacksections=['tox'])
distshare_default = "{toxworkdir}/distshare"
elif not ctxname:
- reader = IniReader(self._cfg)
- toxsection = "tox"
+ reader = SectionReader("tox", self._cfg)
distshare_default = "{homedir}/.tox/distshare"
else:
raise ValueError("invalid context")
@@ -260,18 +541,17 @@
reader.addsubstitutions(toxinidir=config.toxinidir,
homedir=config.homedir)
- config.toxworkdir = reader.getpath(toxsection, "toxworkdir",
- "{toxinidir}/.tox")
- config.minversion = reader.getdefault(toxsection, "minversion", None)
+ config.toxworkdir = reader.getpath("toxworkdir", "{toxinidir}/.tox")
+ config.minversion = reader.getstring("minversion", None)
if not config.option.skip_missing_interpreters:
config.option.skip_missing_interpreters = \
- reader.getbool(toxsection, "skip_missing_interpreters", False)
+ reader.getbool("skip_missing_interpreters", False)
# determine indexserver dictionary
config.indexserver = {'default': IndexServerConfig('default')}
prefix = "indexserver"
- for line in reader.getlist(toxsection, prefix):
+ for line in reader.getlist(prefix):
name, url = map(lambda x: x.strip(), line.split("=", 1))
config.indexserver[name] = IndexServerConfig(name, url)
@@ -296,16 +576,15 @@
config.indexserver[name] = IndexServerConfig(name, override)
reader.addsubstitutions(toxworkdir=config.toxworkdir)
- config.distdir = reader.getpath(toxsection, "distdir", "{toxworkdir}/dist")
+ config.distdir = reader.getpath("distdir", "{toxworkdir}/dist")
reader.addsubstitutions(distdir=config.distdir)
- config.distshare = reader.getpath(toxsection, "distshare",
- distshare_default)
+ config.distshare = reader.getpath("distshare", distshare_default)
reader.addsubstitutions(distshare=config.distshare)
- config.sdistsrc = reader.getpath(toxsection, "sdistsrc", None)
- config.setupdir = reader.getpath(toxsection, "setupdir", "{toxinidir}")
+ 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, toxsection)
+ config.envlist, all_envs = self._getenvdata(reader)
# factors used in config or predefined
known_factors = self._list_section_factors("testenv")
@@ -313,7 +592,7 @@
known_factors.add("python")
# factors stated in config envlist
- stated_envlist = reader.getdefault(toxsection, "envlist", replace=False)
+ stated_envlist = reader.getstring("envlist", replace=False)
if stated_envlist:
for env in _split_env(stated_envlist):
known_factors.update(env.split('-'))
@@ -324,13 +603,13 @@
factors = set(name.split('-'))
if section in self._cfg or factors <= known_factors:
config.envconfigs[name] = \
- self._makeenvconfig(name, section, reader._subs, config)
+ self.make_envconfig(name, section, reader._subs, config)
all_develop = all(name in config.envconfigs
- and config.envconfigs[name].develop
+ and config.envconfigs[name].usedevelop
for name in config.envlist)
- config.skipsdist = reader.getbool(toxsection, "skipsdist", all_develop)
+ config.skipsdist = reader.getbool("skipsdist", all_develop)
def _list_section_factors(self, section):
factors = set()
@@ -340,116 +619,42 @@
factors.update(*mapcat(_split_factor_expr, exprs))
return factors
- def _makeenvconfig(self, name, section, subs, config):
+ def make_envconfig(self, name, section, subs, config):
vc = VenvConfig(config=config, envname=name)
factors = set(name.split('-'))
- reader = IniReader(self._cfg, fallbacksections=["testenv"], factors=factors)
+ reader = SectionReader(section, self._cfg, fallbacksections=["testenv"],
+ factors=factors)
reader.addsubstitutions(**subs)
- vc.develop = (
- not config.option.installpkg
- and reader.getbool(section, "usedevelop", config.option.develop))
- vc.envdir = reader.getpath(section, "envdir", "{toxworkdir}/%s" % name)
- vc.args_are_paths = reader.getbool(section, "args_are_paths", True)
- if reader.getdefault(section, "python", None):
- raise tox.exception.ConfigError(
- "'python=' key was renamed to 'basepython='")
- bp = next((default_factors[f] for f in factors if f in default_factors),
- sys.executable)
- vc.basepython = reader.getdefault(section, "basepython", bp)
- vc._basepython_info = config.interpreters.get_info(vc.basepython)
- reader.addsubstitutions(envdir=vc.envdir, envname=vc.envname,
- envbindir=vc.envbindir, envpython=vc.envpython,
- envsitepackagesdir=vc.envsitepackagesdir)
- vc.envtmpdir = reader.getpath(section, "tmpdir", "{envdir}/tmp")
- vc.envlogdir = reader.getpath(section, "envlogdir", "{envdir}/log")
- reader.addsubstitutions(envlogdir=vc.envlogdir, envtmpdir=vc.envtmpdir)
- vc.changedir = reader.getpath(section, "changedir", "{toxinidir}")
- if config.option.recreate:
- vc.recreate = True
- else:
- vc.recreate = reader.getbool(section, "recreate", False)
- args = config.option.args
- if args:
- if vc.args_are_paths:
- args = []
- for arg in config.option.args:
- if arg:
- origpath = config.invocationcwd.join(arg, abs=True)
- if origpath.check():
- arg = vc.changedir.bestrelpath(origpath)
- args.append(arg)
- reader.addsubstitutions(args)
- setenv = {}
- if config.hashseed is not None:
- setenv['PYTHONHASHSEED'] = config.hashseed
- setenv.update(reader.getdict(section, 'setenv'))
+ reader.addsubstitutions(envname=name)
- # read passenv
- vc.passenv = set(["PATH"])
- if sys.platform == "win32":
- vc.passenv.add("SYSTEMROOT") # needed for python's crypto module
- vc.passenv.add("PATHEXT") # needed for discovering executables
- for spec in reader.getlist(section, "passenv", sep=" "):
- for name in os.environ:
- if fnmatchcase(name.lower(), spec.lower()):
- vc.passenv.add(name)
+ for env_attr in config._testenv_attr:
+ atype = env_attr.type
+ if atype in ("bool", "path", "string", "dict", "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,))
- vc.setenv = setenv
- if not vc.setenv:
- vc.setenv = None
+ if env_attr.postprocess:
+ res = env_attr.postprocess(config, reader, res)
+ setattr(vc, env_attr.name, res)
- vc.commands = reader.getargvlist(section, "commands")
- vc.whitelist_externals = reader.getlist(section,
- "whitelist_externals")
- vc.deps = []
- for depline in reader.getlist(section, "deps"):
- 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)
- vc.deps.append(DepConfig(name, ixserver))
+ if atype == "path":
+ reader.addsubstitutions(**{env_attr.name: res})
- platform = ""
- for platform in reader.getlist(section, "platform"):
- if platform.strip():
- break
- vc.platform = platform
-
- vc.sitepackages = (
- self.config.option.sitepackages
- or reader.getbool(section, "sitepackages", False))
-
- vc.downloadcache = None
- downloadcache = reader.getdefault(section, "downloadcache")
- if downloadcache:
- # env var, if present, takes precedence
- downloadcache = os.environ.get("PIP_DOWNLOAD_CACHE", downloadcache)
- vc.downloadcache = py.path.local(downloadcache)
-
- vc.install_command = reader.getargv(
- section,
- "install_command",
- "pip install {opts} {packages}",
- )
- if '{packages}' not in vc.install_command:
- raise tox.exception.ConfigError(
- "'install_command' must contain '{packages}' substitution")
- vc.pip_pre = config.option.pre or reader.getbool(
- section, "pip_pre", False)
-
- vc.skip_install = reader.getbool(section, "skip_install", False)
- vc.ignore_errors = reader.getbool(section, "ignore_errors", False)
-
+ if env_attr.name == "install_command":
+ reader.addsubstitutions(envbindir=vc.envbindir, envpython=vc.envpython,
+ envsitepackagesdir=vc.envsitepackagesdir)
return vc
- def _getenvdata(self, reader, toxsection):
+ def _getenvdata(self, reader):
envstr = self.config.option.env \
or os.environ.get("TOXENV") \
- or reader.getdefault(toxsection, "envlist", replace=False) \
+ or reader.getstring("envlist", replace=False) \
or []
envlist = _split_env(envstr)
@@ -466,32 +671,6 @@
return envlist, all_envs
- 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
- dep2_name = pkg_resources.Requirement.parse(dep2).project_name
- return dep1_name == dep2_name
-
def _split_env(env):
"""if handed a list, action="append" was used for -e """
@@ -558,8 +737,9 @@
re.VERBOSE)
-class IniReader:
- def __init__(self, cfgparser, fallbacksections=None, factors=()):
+class SectionReader:
+ def __init__(self, section_name, cfgparser, fallbacksections=None, factors=()):
+ self.section_name = section_name
self._cfg = cfgparser
self.fallbacksections = fallbacksections or []
self.factors = factors
@@ -571,129 +751,39 @@
if _posargs:
self.posargs = _posargs
- def getpath(self, section, name, defaultpath):
+ def getpath(self, name, defaultpath):
toxinidir = self._subs['toxinidir']
- path = self.getdefault(section, name, defaultpath)
+ path = self.getstring(name, defaultpath)
if path is None:
return path
return toxinidir.join(path, abs=True)
- def getlist(self, section, name, sep="\n"):
- s = self.getdefault(section, name, None)
+ 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, section, name, sep="\n"):
- s = self.getdefault(section, name, None)
+ def getdict(self, name, default=None, sep="\n"):
+ s = self.getstring(name, None)
if s is None:
- return {}
+ return default or {}
value = {}
for line in s.split(sep):
- if not line.strip():
- continue
- name, rest = line.split('=', 1)
- value[name.strip()] = rest.strip()
+ if line.strip():
+ name, rest = line.split('=', 1)
+ value[name.strip()] = rest.strip()
return value
- def getargvlist(self, section, name):
- """Get arguments for every parsed command.
-
- :param str section: Section name in the configuration.
- :param str name: Key name in a section.
- :rtype: list[list[str]]
- :raise :class:`tox.exception.ConfigError`:
- line-continuation ends nowhere while resolving for specified section
- """
- content = self.getdefault(section, name, '', replace=False)
- return self._parse_commands(section, name, content)
-
- def _parse_commands(self, section, name, content):
- """Parse commands from key content in specified section.
-
- :param str section: Section name in the configuration.
- :param str name: Key name in a section.
- :param str content: 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 content.splitlines():
- line = line.rstrip()
- i = line.find("#")
- if i != -1:
- line = line[:i].rstrip()
- if not line:
- continue
- if line.endswith("\\"):
- current_command += " " + line[:-1]
- continue
- current_command += line
-
- if is_section_substitution(current_command):
- replaced = self._replace(current_command)
- commands.extend(self._parse_commands(section, name, replaced))
- else:
- commands.append(self._processcommand(current_command))
- current_command = ""
- else:
- if current_command:
- raise tox.exception.ConfigError(
- "line-continuation ends nowhere while resolving for [%s] %s" %
- (section, name))
- return commands
-
- def _processcommand(self, command):
- posargs = getattr(self, "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 = self._replace(word)
- new_word = self._replace(new_word)
- 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 = ''
- shlexer.commenters = ''
- argv = list(shlexer)
- return argv
-
- def getargv(self, section, name, default=None, replace=True):
- command = self.getdefault(
- section, name, default=default, replace=False)
- return self._processcommand(command.strip())
-
- def getbool(self, section, name, default=None):
- s = self.getdefault(section, name, default)
+ 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" % (
- section, name))
+ self.section_name, name))
if not isinstance(s, bool):
if s.lower() == "true":
@@ -705,9 +795,16 @@
"boolean value %r needs to be 'True' or 'False'")
return s
- def getdefault(self, section, name, default=None, replace=True):
+ 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):
x = None
- for s in [section] + self.fallbacksections:
+ for s in [self.section_name] + self.fallbacksections:
try:
x = self._cfg[s][name]
break
@@ -720,12 +817,12 @@
x = self._apply_factors(x)
if replace and x and hasattr(x, 'replace'):
- self._subststack.append((section, name))
+ self._subststack.append((self.section_name, name))
try:
x = self._replace(x)
finally:
- assert self._subststack.pop() == (section, name)
- # print "getdefault", section, name, "returned", repr(x)
+ assert self._subststack.pop() == (self.section_name, name)
+ # print "getstring", self.section_name, name, "returned", repr(x)
return x
def _apply_factors(self, s):
@@ -821,8 +918,80 @@
return RE_ITEM_REF.sub(self._replace_match, x)
return x
- def _parse_command(self, command):
- pass
+
+class _ArgvlistReader:
+ @classmethod
+ def getargvlist(cls, reader, section_val):
+ """Parse ``commands`` argvlist multiline string.
+
+ :param str name: Key name in a section.
+ :param str section_val: 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 section_val.splitlines():
+ line = line.rstrip()
+ i = line.find("#")
+ if i != -1:
+ line = line[:i].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)
+ 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_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 = ''
+ shlexer.commenters = ''
+ argv = list(shlexer)
+ return argv
class CommandParser(object):
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tox/_venv.py
--- a/tox/_venv.py
+++ b/tox/_venv.py
@@ -10,17 +10,17 @@
class CreationConfig:
def __init__(self, md5, python, version, sitepackages,
- develop, deps):
+ usedevelop, deps):
self.md5 = md5
self.python = python
self.version = version
self.sitepackages = sitepackages
- self.develop = develop
+ self.usedevelop = usedevelop
self.deps = deps
def writeconfig(self, path):
lines = ["%s %s" % (self.md5, self.python)]
- lines.append("%s %d %d" % (self.version, self.sitepackages, self.develop))
+ lines.append("%s %d %d" % (self.version, self.sitepackages, self.usedevelop))
for dep in self.deps:
lines.append("%s %s" % dep)
path.ensure()
@@ -32,14 +32,14 @@
lines = path.readlines(cr=0)
value = lines.pop(0).split(None, 1)
md5, python = value
- version, sitepackages, develop = lines.pop(0).split(None, 3)
+ version, sitepackages, usedevelop = lines.pop(0).split(None, 3)
sitepackages = bool(int(sitepackages))
- develop = bool(int(develop))
+ usedevelop = bool(int(usedevelop))
deps = []
for line in lines:
md5, depstring = line.split(None, 1)
deps.append((md5, depstring))
- return CreationConfig(md5, python, version, sitepackages, develop, deps)
+ return CreationConfig(md5, python, version, sitepackages, usedevelop, deps)
except Exception:
return None
@@ -48,7 +48,7 @@
and self.python == other.python
and self.version == other.version
and self.sitepackages == other.sitepackages
- and self.develop == other.develop
+ and self.usedevelop == other.usedevelop
and self.deps == other.deps)
@@ -143,11 +143,11 @@
self.envconfig.deps, v)
def _getliveconfig(self):
- python = self.envconfig._basepython_info.executable
+ python = self.envconfig.python_info.executable
md5 = getdigest(python)
version = tox.__version__
sitepackages = self.envconfig.sitepackages
- develop = self.envconfig.develop
+ develop = self.envconfig.usedevelop
deps = []
for dep in self._getresolvedeps():
raw_dep = dep.name
@@ -321,11 +321,13 @@
for envname in self.envconfig.passenv:
if envname in os.environ:
env[envname] = os.environ[envname]
- setenv = self.envconfig.setenv
- if setenv:
- env.update(setenv)
+
+ env.update(self.envconfig.setenv)
+
env['VIRTUAL_ENV'] = str(self.path)
+
env.update(extraenv)
+
return env
def test(self, redirect=False):
diff -r d7d35b623979d2e8186798c5bd29a679ef825c56 -r 06bb44d51a4c4d02f643da800d47a7197cfab32c tox/hookspecs.py
--- /dev/null
+++ b/tox/hookspecs.py
@@ -0,0 +1,32 @@
+""" Hook specifications for tox.
+
+"""
+
+from pluggy import HookspecMarker, HookimplMarker
+
+hookspec = HookspecMarker("tox")
+hookimpl = HookimplMarker("tox")
+
+
+ at hookspec
+def tox_addoption(parser):
+ """ add command line options to the argparse-style parser object."""
+
+
+ at hookspec
+def tox_configure(config):
+ """ called after command line options have been parsed and the ini-file has
+ been read. Please be aware that the config object layout may change as its
+ API was not designed yet wrt to providing stability (it was an internal
+ thing purely before tox-2.0). """
+
+
+ at hookspec(firstresult=True)
+def tox_get_python_executable(envconfig):
+ """ return a python executable for the given python base name.
+ The first plugin/hook which returns an executable path will determine it.
+
+ ``envconfig`` is the testenv configuration which contains
+ per-testenv configuration, notably the ``.envname`` and ``.basepython``
+ setting.
+ """
This diff is so big that we needed to truncate the remainder.
Repository URL: https://bitbucket.org/hpk42/tox/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
More information about the pytest-commit
mailing list