From 89b00d714f66cf9cab9863c62978217bf811fed8 Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Thu, 25 Jun 2015 14:53:54 +0200 Subject: [PATCH 01/34] deprecated_call dit not revert functions back to original Make sure that the warnings.warn and warnings.warn_explicit are reverted to there original function after running the function in deprecated_call --- _pytest/recwarn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 482b78b0e..875cb510e 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -45,8 +45,8 @@ def deprecated_call(func, *args, **kwargs): try: ret = func(*args, **kwargs) finally: - warnings.warn_explicit = warn_explicit - warnings.warn = warn + warnings.warn_explicit = oldwarn_explicit + warnings.warn = oldwarn if not l: __tracebackhide__ = True raise AssertionError("%r did not produce DeprecationWarning" %(func,)) From 444cdfe6e3ba1fb5788da9bb6778cfec76970be4 Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Thu, 25 Jun 2015 17:33:40 +0200 Subject: [PATCH 02/34] Correct test_deprecated_call_preserves test. Test if we preserve the correct functions. --- testing/test_recwarn.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index d8fe1784e..d3455946f 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -64,12 +64,12 @@ def test_deprecated_call_ret(): assert ret == 42 def test_deprecated_call_preserves(): - r = py.std.warnings.onceregistry.copy() - f = py.std.warnings.filters[:] + warn = py.std.warnings.warn + warn_explicit = py.std.warnings.warn_explicit test_deprecated_call_raises() test_deprecated_call() - assert r == py.std.warnings.onceregistry - assert f == py.std.warnings.filters + assert warn == py.std.warnings.warn + assert warn_explicit == py.std.warnings.warn_explicit def test_deprecated_explicit_call_raises(): pytest.raises(AssertionError, From 75679f08c9622cc9112f2fa4dfd2f8d295cf6643 Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Thu, 25 Jun 2015 17:38:45 +0200 Subject: [PATCH 03/34] Update AUTHORS and CHANGELOG --- AUTHORS | 1 + CHANGELOG | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AUTHORS b/AUTHORS index be4cd290d..ca82b483f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -42,6 +42,7 @@ Marc Schlaich Mark Abramowitz Martijn Faassen Nicolas Delaby +Pieter Mulder Piotr Banaszkiewicz Punyashloka Biswal Ralf Schmitt diff --git a/CHANGELOG b/CHANGELOG index 1023fd95f..88d21d684 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 2.7.2 (compared to 2.7.1) ----------------------------- +- preserve warning functions after call to pytest.deprecated_call + - fix issue767: pytest.raises value attribute does not contain the exception instance on Python 2.6. Thanks Eric Siegerman for providing the test case and Bruno Oliveira for PR. From 9fb1637ce2320de792ebc16afca45e10caf65d8f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 26 Jun 2015 00:26:59 -0300 Subject: [PATCH 04/34] Test that deprecated_call keeps internal warnings structures intact --- testing/test_recwarn.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index d3455946f..65989cb3c 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -64,12 +64,16 @@ def test_deprecated_call_ret(): assert ret == 42 def test_deprecated_call_preserves(): + onceregistry = py.std.warnings.onceregistry.copy() + filters = py.std.warnings.filters[:] warn = py.std.warnings.warn warn_explicit = py.std.warnings.warn_explicit test_deprecated_call_raises() test_deprecated_call() - assert warn == py.std.warnings.warn - assert warn_explicit == py.std.warnings.warn_explicit + assert onceregistry == py.std.warnings.onceregistry + assert filters == py.std.warnings.filters + assert warn is py.std.warnings.warn + assert warn_explicit is py.std.warnings.warn_explicit def test_deprecated_explicit_call_raises(): pytest.raises(AssertionError, From 27a98788a81703b7587c73b6423c72302355ec89 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 26 Jun 2015 00:30:26 -0300 Subject: [PATCH 05/34] start 2.7.3 CHANGELOG --- CHANGELOG | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 88d21d684..9484b0b75 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,11 @@ -2.7.2 (compared to 2.7.1) +2.7.3 (compared to 2.7.2) ----------------------------- -- preserve warning functions after call to pytest.deprecated_call +- preserve warning functions after call to pytest.deprecated_call. Thanks + Pieter Mulder for PR. + +2.7.2 (compared to 2.7.1) +----------------------------- - fix issue767: pytest.raises value attribute does not contain the exception instance on Python 2.6. Thanks Eric Siegerman for providing the test From 5ec2a17f08f3fa51a32607b174c96b5801d39551 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 12 Jul 2015 17:32:39 -0300 Subject: [PATCH 06/34] --fixtures only shows fixtures from first file Fix #833 --- CHANGELOG | 7 ++++++- _pytest/python.py | 10 +--------- testing/python/fixture.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9484b0b75..7d48d5444 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,11 @@ - preserve warning functions after call to pytest.deprecated_call. Thanks Pieter Mulder for PR. +- fix issue833: --fixtures does not show fixtures defined in test files, + except for the first one collected. Thanks Florian Bruhin for reporting + and Bruno Oliveira for the PR. + + 2.7.2 (compared to 2.7.1) ----------------------------- @@ -27,7 +32,7 @@ Thanks Thomas De Schampheleire for reporting and Bruno Oliveira for the PR. - fix issue718: failed to create representation of sets containing unsortable - elements in python 2. Thanks Edison Gustavo Muenz + elements in python 2. Thanks Edison Gustavo Muenz. - fix issue756, fix issue752 (and similar issues): depend on py-1.4.29 which has a refined algorithm for traceback generation. diff --git a/_pytest/python.py b/_pytest/python.py index 2fa4af373..052a07784 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -939,21 +939,13 @@ def showfixtures(config): def _showfixtures_main(config, session): session.perform_collect() curdir = py.path.local() - if session.items: - nodeid = session.items[0].nodeid - else: - part = session._initialparts[0] - nodeid = "::".join(map(str, [curdir.bestrelpath(part[0])] + part[1:])) - nodeid.replace(session.fspath.sep, "/") - tw = py.io.TerminalWriter() verbose = config.getvalue("verbose") fm = session._fixturemanager available = [] - for argname in fm._arg2fixturedefs: - fixturedefs = fm.getfixturedefs(argname, nodeid) + for argname, fixturedefs in fm._arg2fixturedefs.items(): assert fixturedefs is not None if not fixturedefs: continue diff --git a/testing/python/fixture.py b/testing/python/fixture.py index ef43744d5..69c140f8d 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2483,6 +2483,44 @@ class TestShowFixtures: """) + def test_show_fixtures_different_files(self, testdir): + """ + #833: --fixtures only shows fixtures from first file + """ + testdir.makepyfile(test_a=''' + import pytest + + @pytest.fixture + def fix_a(): + """Fixture A""" + pass + + def test_a(fix_a): + pass + ''') + testdir.makepyfile(test_b=''' + import pytest + + @pytest.fixture + def fix_b(): + """Fixture B""" + pass + + def test_b(fix_b): + pass + ''') + result = testdir.runpytest("--fixtures") + result.stdout.fnmatch_lines(""" + * fixtures defined from test_a * + fix_a + Fixture A + + * fixtures defined from test_b * + fix_b + Fixture B + """) + + class TestContextManagerFixtureFuncs: def test_simple(self, testdir): testdir.makepyfile(""" From 5750ae784a11bc1d92f61646e7f5c58dc99bf63e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 13 Jul 2015 12:01:54 -0300 Subject: [PATCH 07/34] Reword CHANGELOG a bit --- CHANGELOG | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7d48d5444..535d1baa9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,9 +4,9 @@ - preserve warning functions after call to pytest.deprecated_call. Thanks Pieter Mulder for PR. -- fix issue833: --fixtures does not show fixtures defined in test files, - except for the first one collected. Thanks Florian Bruhin for reporting - and Bruno Oliveira for the PR. +- fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the + fixtures declared on the first one. + Thanks Florian Bruhin for reporting and Bruno Oliveira for the PR. 2.7.2 (compared to 2.7.1) From 7a71b69a87fb53d6478869cceb2b61f4f6db185f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Jul 2015 19:57:48 -0300 Subject: [PATCH 08/34] Pinning mock module to < 1.1 fo py26 It has been announced that mock>=1.1 will be supported for python 2.7 only. Conflicts: tox.ini --- tox.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tox.ini b/tox.ini index b0afd479a..f5528bb17 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,12 @@ deps= nose mock +[testenv:py26] +commands= py.test --lsof -rfsxX {posargs:testing} +deps= + nose + mock<1.1 # last supported version for py26 + [testenv:genscript] commands= py.test --genscript=pytest1 From ebf32ae8a92d1376802d870a2641e4356311ed71 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 15 Jun 2015 18:54:44 -0300 Subject: [PATCH 09/34] Update CONTRIBUTING documentation with GitHub instructions --- CONTRIBUTING.rst | 75 +++++++++++++++---------------------- doc/en/img/pullrequest.png | Bin 23823 -> 17035 bytes 2 files changed, 30 insertions(+), 45 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index b7d829a30..587b309a6 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -17,10 +17,10 @@ Submit a plugin, co-develop pytest Pytest development of the core, some plugins and support code happens in repositories living under: -- `the pytest-dev bitbucket team `_ - - `the pytest-dev github organisation `_ +- `the pytest-dev bitbucket team `_ + All pytest-dev team members have write access to all contained repositories. pytest core and plugins are generally developed using `pull requests`_ to respective repositories. @@ -56,7 +56,7 @@ right to release to pypi. Report bugs ----------- -Report bugs for pytest at https://bitbucket.org/pytest-dev/pytest/issues +Report bugs for pytest at https://github.com/pytest-dev/pytest/issues If you are reporting a bug, please include: @@ -74,7 +74,7 @@ Submit feedback for developers Do you like pytest? Share some love on Twitter or in your blog posts! We'd also like to hear about your propositions and suggestions. Feel free to -`submit them as issues `__ and: +`submit them as issues `__ and: * Set the "kind" to "enhancement" or "proposal" so that we can quickly find about them. @@ -88,8 +88,8 @@ We'd also like to hear about your propositions and suggestions. Feel free to Fix bugs -------- -Look through the BitBucket issues for bugs. Here is sample filter you can use: -https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open&kind=bug +Look through the GitHub issues for bugs. Here is sample filter you can use: +https://github.com/pytest-dev/pytest/labels/bug :ref:`Talk ` to developers to find out how you can fix specific bugs. @@ -98,9 +98,9 @@ https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open&kind=bug Implement features ------------------ -Look through the BitBucket issues for enhancements. Here is sample filter you +Look through the GitHub issues for enhancements. Here is sample filter you can use: -https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open&kind=enhancement +https://github.com/pytest-dev/pytest/labels/enhancement :ref:`Talk ` to developers to find out how you can implement specific features. @@ -118,35 +118,35 @@ pytest could always use more documentation. What exactly is needed? .. _`pull requests`: .. _pull-requests: -Preparing Pull Requests on Bitbucket ------------------------------------- +Preparing Pull Requests on GitHub +--------------------------------- .. note:: What is a "pull request"? It informs project's core developers about the changes you want to review and merge. Pull requests are stored on - `BitBucket servers `__. + `GitHub servers `_. Once you send pull request, we can discuss it's potential modifications and even add more commits to it later on. -The primary development platform for pytest is BitBucket. You can find all -the issues there and submit your pull requests. +There's an excellent tutorial on how Pull Requests work in the +`GitHub Help Center `_, +but here is a simple overview: #. Fork the - `pytest BitBucket repository `__. It's + `pytest GitHub repository `__. It's fine to use ``pytest`` as your fork repository name because it will live under your user. -#. Clone your fork locally using `Mercurial `_ - (``hg``) and create a branch:: +#. Clone your fork locally using `git `_ and create a branch:: - $ hg clone ssh://hg@bitbucket.org/YOUR_BITBUCKET_USERNAME/pytest + $ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git $ cd pytest - $ hg up pytest-2.7 # if you want to fix a bug for the pytest-2.7 series - $ hg up default # if you want to add a feature bound for the next minor release - $ hg branch your-branch-name # your feature/bugfix branch + $ git checkout pytest-2.7 # if you want to fix a bug for the pytest-2.7 series + $ git checkout master # if you want to add a feature bound for the next minor release + $ git branch your-branch-name # your feature/bugfix branch - If you need some help with Mercurial, follow this quick start - guide: http://mercurial.selenic.com/wiki/QuickStart + If you need some help with Git, follow this quick start + guide: https://git.wiki.kernel.org/index.php/QuickStart #. Create a development environment (will implicitly use http://www.virtualenv.org/en/latest/):: @@ -178,10 +178,10 @@ the issues there and submit your pull requests. #. Commit and push once your tests pass and you are happy with your change(s):: - $ hg commit -m"" - $ hg push -b . + $ git commit -a -m "" + $ git push -u -#. Finally, submit a pull request through the BitBucket website: +#. Finally, submit a pull request through the GitHub website: .. image:: img/pullrequest.png :width: 700px @@ -189,26 +189,11 @@ the issues there and submit your pull requests. :: - source: YOUR_BITBUCKET_USERNAME/pytest - branch: your-branch-name + head-fork: YOUR_GITHUB_USERNAME/pytest + compare: your-branch-name - target: pytest-dev/pytest - branch: default # if it's a feature - branch: pytest-VERSION # if it's a bugfix + base-fork: pytest-dev/pytest + base: master # if it's a feature + base: pytest-VERSION # if it's a bugfix -.. _contribution-using-git: - -Using git with bitbucket/hg -------------------------------- - -There used to be the pytest GitHub mirror. It was removed in favor of the -Mercurial one, to remove confusion of people not knowing where it's better to -put their issues and pull requests. Also it wasn't easily possible to automate -the mirroring process. - -In general we recommend to work with the same version control system of the -original repository. If you insist on using git with bitbucket/hg you -may try `gitifyhg `_ but are on your -own and need to submit pull requests through the respective platform, -nevertheless. diff --git a/doc/en/img/pullrequest.png b/doc/en/img/pullrequest.png index e0724d764d646e7381f4283c017e0e0b0adaf9bf..4af293b21377078de648f82e12c7b5d2745da837 100644 GIT binary patch literal 17035 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 literal 23823 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 Dk() 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 MMO(T2*{iVs0o(af+W-In From 94cdec2cfecc1c4023e2f088d431c92d56af3464 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 15 Jun 2015 19:18:03 -0300 Subject: [PATCH 10/34] Update sidebar links from Bitbucket to GitHub --- doc/en/_templates/links.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/_templates/links.html b/doc/en/_templates/links.html index 9e23a8056..2f9791fd3 100644 --- a/doc/en/_templates/links.html +++ b/doc/en/_templates/links.html @@ -3,9 +3,9 @@
  • The pytest Website
  • Contribution Guide
  • pytest @ PyPI
  • -
  • pytest @ Bitbucket
  • +
  • pytest @ GitHub
  • 3rd party plugins
  • -
  • Issue Tracker
  • +
  • Issue Tracker
  • PDF Documentation From aa25fb05a9a4376d206a44e01c0e01fa21604f75 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Jul 2015 21:03:30 -0300 Subject: [PATCH 11/34] Make sure marks in subclasses don't change marks in superclasses Fix #842 --- CHANGELOG | 4 ++++ _pytest/mark.py | 19 ++++++++------- testing/test_mark.py | 55 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 535d1baa9..d9fa3a4f0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ 2.7.3 (compared to 2.7.2) ----------------------------- +- fix issue842: applying markers in classes no longer propagate this markers + to superclasses which also have markers. + Thanks xmo-odoo for the report and Bruno Oliveira for the PR. + - preserve warning functions after call to pytest.deprecated_call. Thanks Pieter Mulder for PR. diff --git a/_pytest/mark.py b/_pytest/mark.py index 1d5043578..791f6ef55 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -1,4 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ +import inspect import py @@ -253,15 +254,17 @@ class MarkDecorator: otherwise add *args/**kwargs in-place to mark information. """ if args and not kwargs: func = args[0] - if len(args) == 1 and (istestfunc(func) or - hasattr(func, '__bases__')): - if hasattr(func, '__bases__'): + is_class = inspect.isclass(func) + if len(args) == 1 and (istestfunc(func) or is_class): + if is_class: if hasattr(func, 'pytestmark'): - l = func.pytestmark - if not isinstance(l, list): - func.pytestmark = [l, self] - else: - l.append(self) + mark_list = func.pytestmark + if not isinstance(mark_list, list): + mark_list = [mark_list] + # always work on a copy to avoid updating pytestmark + # from a superclass by accident + mark_list = mark_list + [self] + func.pytestmark = mark_list else: func.pytestmark = [self] else: diff --git a/testing/test_mark.py b/testing/test_mark.py index a7ee038ea..3527deb1e 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -369,6 +369,45 @@ class TestFunctional: print (item, item.keywords) assert 'a' in item.keywords + def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): + p = testdir.makepyfile(""" + import pytest + + @pytest.mark.a + class Base: pass + + @pytest.mark.b + class Test1(Base): + def test_foo(self): pass + + class Test2(Base): + def test_bar(self): pass + """) + items, rec = testdir.inline_genitems(p) + self.assert_markers(items, test_foo=('a', 'b'), test_bar=('a',)) + + def test_mark_decorator_baseclasses_merged(self, testdir): + p = testdir.makepyfile(""" + import pytest + + @pytest.mark.a + class Base: pass + + @pytest.mark.b + class Base2(Base): pass + + @pytest.mark.c + class Test1(Base2): + def test_foo(self): pass + + class Test2(Base2): + @pytest.mark.d + def test_bar(self): pass + """) + items, rec = testdir.inline_genitems(p) + self.assert_markers(items, test_foo=('a', 'b', 'c'), + test_bar=('a', 'b', 'd')) + def test_mark_with_wrong_marker(self, testdir): reprec = testdir.inline_runsource(""" import pytest @@ -477,6 +516,22 @@ class TestFunctional: reprec = testdir.inline_run("-m", "mark1") reprec.assertoutcome(passed=1) + def assert_markers(self, items, **expected): + """assert that given items have expected marker names applied to them. + expected should be a dict of (item name -> seq of expected marker names) + + .. note:: this could be moved to ``testdir`` if proven to be useful + to other modules. + """ + from _pytest.mark import MarkInfo + items = dict((x.name, x) for x in items) + for name, expected_markers in expected.items(): + markers = items[name].keywords._markers + marker_names = set([name for (name, v) in markers.items() + if isinstance(v, MarkInfo)]) + assert marker_names == set(expected_markers) + + class TestKeywordSelection: def test_select_simple(self, testdir): file_test = testdir.makepyfile(""" From 330de0a93db404921c7a4c80305b43fd87bc3119 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 14 Jul 2015 18:11:30 -0300 Subject: [PATCH 12/34] Use a subdirectory in the TEMP directory to speed up tmpdir creation Fix #105 --- CHANGELOG | 6 ++++++ _pytest/tmpdir.py | 9 ++++++++- tox.ini | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d9fa3a4f0..8b6feb3a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,12 @@ fixtures declared on the first one. Thanks Florian Bruhin for reporting and Bruno Oliveira for the PR. +- optimized tmpdir fixture initialization, which should make test sessions + faster (specially when using pytest-xdist). The only visible effect + is that now pytest uses a subdirectory in the $TEMP directory for all + directories created by this fixture (defaults to $TEMP/pytest-$USER). + Thanks Bruno Oliveira for the PR. + 2.7.2 (compared to 2.7.1) ----------------------------- diff --git a/_pytest/tmpdir.py b/_pytest/tmpdir.py index 53c396b76..5e83ec931 100644 --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -43,7 +43,14 @@ class TempdirHandler: basetemp.remove() basetemp.mkdir() else: - basetemp = py.path.local.make_numbered_dir(prefix='pytest-') + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + import getpass + temproot = py.path.local.get_temproot() + rootdir = temproot.join('pytest-%s' % getpass.getuser()) + rootdir.ensure(dir=1) + basetemp = py.path.local.make_numbered_dir(prefix='pytest-', + rootdir=rootdir) self._basetemp = t = basetemp.realpath() self.trace("new basetemp", t) return t diff --git a/tox.ini b/tox.ini index f5528bb17..2e1a15b8b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist= [testenv] commands= py.test --lsof -rfsxX {posargs:testing} +passenv = USER USERNAME deps= nose mock From dcdc823dd2c79688d7da27e9393cda4fd4323ac0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 2 Jul 2015 23:13:59 -0300 Subject: [PATCH 13/34] Support for tests created with functools.partial Fix #811 --- _pytest/python.py | 30 +++++++++++++++++++++----- testing/python/collect.py | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 052a07784..eafaf7fb8 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -18,10 +18,19 @@ callable = py.builtin.callable # used to work around a python2 exception info leak exc_clear = getattr(sys, 'exc_clear', lambda: None) -def getfslineno(obj): - # xxx let decorators etc specify a sane ordering +def get_real_func(obj): + """gets the real function object of the (possibly) wrapped object by + functools.wraps or functools.partial. + """ while hasattr(obj, "__wrapped__"): obj = obj.__wrapped__ + if isinstance(obj, py.std.functools.partial): + obj = obj.func + return obj + +def getfslineno(obj): + # xxx let decorators etc specify a sane ordering + obj = get_real_func(obj) if hasattr(obj, 'place_as'): obj = obj.place_as fslineno = py.code.getfslineno(obj) @@ -594,7 +603,10 @@ class FunctionMixin(PyobjMixin): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - code = py.code.Code(self.obj) + if isinstance(self.obj, py.std.functools.partial): + code = py.code.Code(self.obj.func) + else: + code = py.code.Code(self.obj) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -1537,7 +1549,7 @@ class FixtureLookupError(LookupError): for function in stack: fspath, lineno = getfslineno(function) try: - lines, _ = inspect.getsourcelines(function) + lines, _ = inspect.getsourcelines(get_real_func(function)) except IOError: error_msg = "file %s, line %s: source code not available" addline(error_msg % (fspath, lineno+1)) @@ -1937,7 +1949,15 @@ def getfuncargnames(function, startindex=None): if realfunction != function: startindex += num_mock_patch_args(function) function = realfunction - argnames = inspect.getargs(py.code.getrawcode(function))[0] + if isinstance(function, py.std.functools.partial): + argnames = inspect.getargs(py.code.getrawcode(function.func))[0] + partial = function + argnames = argnames[len(partial.args):] + if partial.keywords: + for kw in partial.keywords: + argnames.remove(kw) + else: + argnames = inspect.getargs(py.code.getrawcode(function))[0] defaults = getattr(function, 'func_defaults', getattr(function, '__defaults__', None)) or () numdefaults = len(defaults) diff --git a/testing/python/collect.py b/testing/python/collect.py index bdea33a7f..c7292829f 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -851,3 +851,47 @@ def test_unorderable_types(testdir): result = testdir.runpytest() assert "TypeError" not in result.stdout.str() assert result.ret == 0 + + +def test_collect_functools_partial(testdir): + """ + Test that collection of functools.partial object works, and arguments + to the wrapped functions are dealt correctly (see #811). + """ + testdir.makepyfile(""" + import functools + import pytest + + @pytest.fixture + def fix1(): + return 'fix1' + + @pytest.fixture + def fix2(): + return 'fix2' + + def check1(i, fix1): + assert i == 2 + assert fix1 == 'fix1' + + def check2(fix1, i): + assert i == 2 + assert fix1 == 'fix1' + + def check3(fix1, i, fix2): + assert i == 2 + assert fix1 == 'fix1' + assert fix2 == 'fix2' + + test_ok_1 = functools.partial(check1, i=2) + test_ok_2 = functools.partial(check1, i=2, fix1='fix1') + test_ok_3 = functools.partial(check1, 2) + test_ok_4 = functools.partial(check2, i=2) + test_ok_5 = functools.partial(check3, i=2) + test_ok_6 = functools.partial(check3, i=2, fix1='fix1') + + test_fail_1 = functools.partial(check2, 2) + test_fail_2 = functools.partial(check3, 2) + """) + result = testdir.inline_run() + result.assertoutcome(passed=6, failed=2) From a7b4ed89da6450b1eb93b785dce5f8f1a81c00c8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 13 Jul 2015 12:16:51 -0300 Subject: [PATCH 14/34] Use functools.partial name explicitly and simplify the code a bit as asked in review --- _pytest/python.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index eafaf7fb8..76a6c4ed4 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1,5 +1,6 @@ """ Python test discovery, setup and run of test functions. """ import fnmatch +import functools import py import inspect import sys @@ -24,7 +25,7 @@ def get_real_func(obj): """ while hasattr(obj, "__wrapped__"): obj = obj.__wrapped__ - if isinstance(obj, py.std.functools.partial): + if isinstance(obj, functools.partial): obj = obj.func return obj @@ -603,10 +604,7 @@ class FunctionMixin(PyobjMixin): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - if isinstance(self.obj, py.std.functools.partial): - code = py.code.Code(self.obj.func) - else: - code = py.code.Code(self.obj) + code = py.code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -1949,7 +1947,7 @@ def getfuncargnames(function, startindex=None): if realfunction != function: startindex += num_mock_patch_args(function) function = realfunction - if isinstance(function, py.std.functools.partial): + if isinstance(function, functools.partial): argnames = inspect.getargs(py.code.getrawcode(function.func))[0] partial = function argnames = argnames[len(partial.args):] From 85f7aa2f9b14870686b9e068a3de65e3cc18c7f1 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Tue, 16 Jun 2015 02:58:07 +0200 Subject: [PATCH 15/34] appveyor integration --- appveyor.yml | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..cd9475046 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,82 @@ +environment: + global: + # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the + # /E:ON and /V:ON options are not enabled in the batch script intepreter + # See: http://stackoverflow.com/a/13751649/163740 + CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" + + matrix: + + # Pre-installed Python versions, which Appveyor may upgrade to + # a later point release. + + - PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7.x" # currently 2.7.9 + PYTHON_ARCH: "32" + TESTENV: "py27" + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" # currently 2.7.9 + PYTHON_ARCH: "64" + TESTENV: "py27" + + - PYTHON: "C:\\Python33" + PYTHON_VERSION: "3.3.x" # currently 3.3.5 + PYTHON_ARCH: "32" + TESTENV: "py33" + + - PYTHON: "C:\\Python33-x64" + PYTHON_VERSION: "3.3.x" # currently 3.3.5 + PYTHON_ARCH: "64" + TESTENV: "py33" + + - PYTHON: "C:\\Python34" + PYTHON_VERSION: "3.4.x" # currently 3.4.3 + PYTHON_ARCH: "32" + TESTENV: "py34" + + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4.x" # currently 3.4.3 + PYTHON_ARCH: "64" + TESTENV: "py34" + + # Also test a Python version not pre-installed + # See: https://github.com/ogrisel/python-appveyor-demo/issues/10 + + - PYTHON: "C:\\Python266" + PYTHON_VERSION: "2.6.6" + PYTHON_ARCH: "32" + TESTENV: "py26" + + +install: + - ECHO "Filesystem root:" + - ps: "ls \"C:/\"" + + - ECHO "Installed SDKs:" + - ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\"" + + # Install Python (from the official .msi of http://python.org) and pip when + # not already installed. + - ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 } + + # Prepend newly installed Python to the PATH of this build (this cannot be + # done from inside the powershell script as it would require to restart + # the parent CMD process). + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + + # Check that we have the expected version and architecture for Python + - "python --version" + - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" + + # Install the build dependencies of the project. If some dependencies contain + # compiled extensions and are not provided as pre-built wheel packages, + # pip will build them from source using the MSVC compiler matching the + # target Python version and architecture + - "%CMD_IN_ENV% pip install tox" + +build: false # Not a C# project, build stuff at the test step instead. + +test_script: + # Build the compiled extension and run the project tests + - "%CMD_IN_ENV% tox -e %TESTENV%" From 360c09a1e7e3ab667c3c4f5092e1d1cb534a064d Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Tue, 16 Jun 2015 03:25:28 +0200 Subject: [PATCH 16/34] appveyor scripts --- appveyor/install.ps1 | 180 ++++++++++++++++++++++++++++++++++++++ appveyor/run_with_env.cmd | 47 ++++++++++ 2 files changed, 227 insertions(+) create mode 100644 appveyor/install.ps1 create mode 100644 appveyor/run_with_env.cmd diff --git a/appveyor/install.ps1 b/appveyor/install.ps1 new file mode 100644 index 000000000..0f165d8bd --- /dev/null +++ b/appveyor/install.ps1 @@ -0,0 +1,180 @@ +# Sample script to install Python and pip under Windows +# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner +# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ + +$MINICONDA_URL = "http://repo.continuum.io/miniconda/" +$BASE_URL = "https://www.python.org/ftp/python/" +$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" +$GET_PIP_PATH = "C:\get-pip.py" + + +function DownloadPython ($python_version, $platform_suffix) { + $webclient = New-Object System.Net.WebClient + $filename = "python-" + $python_version + $platform_suffix + ".msi" + $url = $BASE_URL + $python_version + "/" + $filename + + $basedir = $pwd.Path + "\" + $filepath = $basedir + $filename + if (Test-Path $filename) { + Write-Host "Reusing" $filepath + return $filepath + } + + # Download and retry up to 3 times in case of network transient errors. + Write-Host "Downloading" $filename "from" $url + $retry_attempts = 2 + for($i=0; $i -lt $retry_attempts; $i++){ + try { + $webclient.DownloadFile($url, $filepath) + break + } + Catch [Exception]{ + Start-Sleep 1 + } + } + if (Test-Path $filepath) { + Write-Host "File saved at" $filepath + } else { + # Retry once to get the error message if any at the last try + $webclient.DownloadFile($url, $filepath) + } + return $filepath +} + + +function InstallPython ($python_version, $architecture, $python_home) { + Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home + if (Test-Path $python_home) { + Write-Host $python_home "already exists, skipping." + return $false + } + if ($architecture -eq "32") { + $platform_suffix = "" + } else { + $platform_suffix = ".amd64" + } + $msipath = DownloadPython $python_version $platform_suffix + Write-Host "Installing" $msipath "to" $python_home + $install_log = $python_home + ".log" + $install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home" + $uninstall_args = "/qn /x $msipath" + RunCommand "msiexec.exe" $install_args + if (-not(Test-Path $python_home)) { + Write-Host "Python seems to be installed else-where, reinstalling." + RunCommand "msiexec.exe" $uninstall_args + RunCommand "msiexec.exe" $install_args + } + if (Test-Path $python_home) { + Write-Host "Python $python_version ($architecture) installation complete" + } else { + Write-Host "Failed to install Python in $python_home" + Get-Content -Path $install_log + Exit 1 + } +} + +function RunCommand ($command, $command_args) { + Write-Host $command $command_args + Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru +} + + +function InstallPip ($python_home) { + $pip_path = $python_home + "\Scripts\pip.exe" + $python_path = $python_home + "\python.exe" + if (-not(Test-Path $pip_path)) { + Write-Host "Installing pip..." + $webclient = New-Object System.Net.WebClient + $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) + Write-Host "Executing:" $python_path $GET_PIP_PATH + Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru + } else { + Write-Host "pip already installed." + } +} + + +function DownloadMiniconda ($python_version, $platform_suffix) { + $webclient = New-Object System.Net.WebClient + if ($python_version -eq "3.4") { + $filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe" + } else { + $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe" + } + $url = $MINICONDA_URL + $filename + + $basedir = $pwd.Path + "\" + $filepath = $basedir + $filename + if (Test-Path $filename) { + Write-Host "Reusing" $filepath + return $filepath + } + + # Download and retry up to 3 times in case of network transient errors. + Write-Host "Downloading" $filename "from" $url + $retry_attempts = 2 + for($i=0; $i -lt $retry_attempts; $i++){ + try { + $webclient.DownloadFile($url, $filepath) + break + } + Catch [Exception]{ + Start-Sleep 1 + } + } + if (Test-Path $filepath) { + Write-Host "File saved at" $filepath + } else { + # Retry once to get the error message if any at the last try + $webclient.DownloadFile($url, $filepath) + } + return $filepath +} + + +function InstallMiniconda ($python_version, $architecture, $python_home) { + Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home + if (Test-Path $python_home) { + Write-Host $python_home "already exists, skipping." + return $false + } + if ($architecture -eq "32") { + $platform_suffix = "x86" + } else { + $platform_suffix = "x86_64" + } + $filepath = DownloadMiniconda $python_version $platform_suffix + Write-Host "Installing" $filepath "to" $python_home + $install_log = $python_home + ".log" + $args = "/S /D=$python_home" + Write-Host $filepath $args + Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru + if (Test-Path $python_home) { + Write-Host "Python $python_version ($architecture) installation complete" + } else { + Write-Host "Failed to install Python in $python_home" + Get-Content -Path $install_log + Exit 1 + } +} + + +function InstallMinicondaPip ($python_home) { + $pip_path = $python_home + "\Scripts\pip.exe" + $conda_path = $python_home + "\Scripts\conda.exe" + if (-not(Test-Path $pip_path)) { + Write-Host "Installing pip..." + $args = "install --yes pip" + Write-Host $conda_path $args + Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru + } else { + Write-Host "pip already installed." + } +} + +function main () { + InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON + InstallPip $env:PYTHON +} + +main diff --git a/appveyor/run_with_env.cmd b/appveyor/run_with_env.cmd new file mode 100644 index 000000000..3a472bc83 --- /dev/null +++ b/appveyor/run_with_env.cmd @@ -0,0 +1,47 @@ +:: To build extensions for 64 bit Python 3, we need to configure environment +:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) +:: +:: To build extensions for 64 bit Python 2, we need to configure environment +:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) +:: +:: 32 bit builds do not require specific environment configurations. +:: +:: Note: this script needs to be run with the /E:ON and /V:ON flags for the +:: cmd interpreter, at least for (SDK v7.0) +:: +:: More details at: +:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows +:: http://stackoverflow.com/a/13751649/163740 +:: +:: Author: Olivier Grisel +:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ +@ECHO OFF + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows + +SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" +IF %MAJOR_PYTHON_VERSION% == "2" ( + SET WINDOWS_SDK_VERSION="v7.0" +) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( + SET WINDOWS_SDK_VERSION="v7.1" +) ELSE ( + ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" + EXIT 1 +) + +IF "%PYTHON_ARCH%"=="64" ( + ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture + SET DISTUTILS_USE_SDK=1 + SET MSSdk=1 + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 +) ELSE ( + ECHO Using default MSVC build environment for 32 bit architecture + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 +) From b18e6439bdf60ed0b9d41dbb65c5ae5da2efd01e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 23 Jun 2015 18:52:05 +0200 Subject: [PATCH 17/34] Ast Call signature changed on 3.5 fix issue 744 on bitbucket port of merge request 296 https://bitbucket.org/pytest-dev/pytest/pull-request/296/astcall-signature-changed-on-35 https://bitbucket.org/pytest-dev/pytest/issue/744/ Conflicts: CHANGELOG --- .travis.yml | 1 + CHANGELOG | 4 +++ _pytest/assertion/rewrite.py | 62 +++++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a6fc5757d..4ad059331 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ env: - TESTENV=py26 - TESTENV=py27 - TESTENV=py34 + - TESTENV=py35 - TESTENV=pypy - TESTENV=py27-pexpect - TESTENV=py33-pexpect diff --git a/CHANGELOG b/CHANGELOG index 8b6feb3a2..03c804514 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ 2.7.3 (compared to 2.7.2) ----------------------------- +- fix issue744: fix for ast.Call changes in Python 3.5+. Thanks + Guido van Rossum, Matthias Bussonnier, Stefan Zimmermann and + Thomas Kluyver. + - fix issue842: applying markers in classes no longer propagate this markers to superclasses which also have markers. Thanks xmo-odoo for the report and Bruno Oliveira for the PR. diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 62046d146..29d18d813 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -35,6 +35,12 @@ PYC_TAIL = "." + PYTEST_TAG + PYC_EXT REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2) ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3 +if sys.version_info >= (3,5): + ast_Call = ast.Call +else : + ast_Call = lambda a,b,c : ast.Call(a, b, c, None, None) + + class AssertionRewritingHook(object): """PEP302 Import hook which rewrites asserts.""" @@ -587,7 +593,7 @@ class AssertionRewriter(ast.NodeVisitor): """Call a helper in this module.""" py_name = ast.Name("@pytest_ar", ast.Load()) attr = ast.Attribute(py_name, "_" + name, ast.Load()) - return ast.Call(attr, list(args), [], None, None) + return ast_Call(attr, list(args), []) def builtin(self, name): """Return the builtin called *name*.""" @@ -677,7 +683,7 @@ class AssertionRewriter(ast.NodeVisitor): msg = self.pop_format_context(template) fmt = self.helper("format_explanation", msg) err_name = ast.Name("AssertionError", ast.Load()) - exc = ast.Call(err_name, [fmt], [], None, None) + exc = ast_Call(err_name, [fmt], []) if sys.version_info[0] >= 3: raise_ = ast.Raise(exc, None) else: @@ -697,7 +703,7 @@ class AssertionRewriter(ast.NodeVisitor): def visit_Name(self, name): # Display the repr of the name if it's a local variable or # _should_repr_global_name() thinks it's acceptable. - locs = ast.Call(self.builtin("locals"), [], [], None, None) + locs = ast_Call(self.builtin("locals"), [], []) inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) dorepr = self.helper("should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) @@ -724,7 +730,7 @@ class AssertionRewriter(ast.NodeVisitor): res, expl = self.visit(v) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) expl_format = self.pop_format_context(ast.Str(expl)) - call = ast.Call(app, [expl_format], [], None, None) + call = ast_Call(app, [expl_format], []) self.on_failure.append(ast.Expr(call)) if i < levels: cond = res @@ -753,7 +759,44 @@ class AssertionRewriter(ast.NodeVisitor): res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) return res, explanation - def visit_Call(self, call): + def visit_Call_35(self, call): + """ + visit `ast.Call` nodes on Python3.5 and after + """ + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + for arg in call.args: + if type(arg) is ast.Starred: + new_star, expl = self.visit(arg) + arg_expls.append("*" + expl) + new_args.append(new_star) + else: + res, expl = self.visit(arg) + new_args.append(res) + arg_expls.append(expl) + for keyword in call.keywords: + if keyword.arg: + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + arg_expls.append(keyword.arg + "=" + expl) + else: ## **args have `arg` keywords with an .arg of None + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + arg_expls.append("**" + expl) + + expl = "%s(%s)" % (func_expl, ', '.join(arg_expls)) + new_call = ast.Call(new_func, new_args, new_kwargs) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) + outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) + return res, outer_expl + + def visit_Call_legacy(self, call): + """ + visit `ast.Call nodes on 3.4 and below` + """ new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] @@ -781,6 +824,15 @@ class AssertionRewriter(ast.NodeVisitor): outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) return res, outer_expl + # ast.Call signature changed on 3.5, + # conditionally change which methods is named + # visit_Call depending on Python version + if sys.version_info >= (3, 5): + visit_Call = visit_Call_35 + else: + visit_Call = visit_Call_legacy + + def visit_Attribute(self, attr): if not isinstance(attr.ctx, ast.Load): return self.generic_visit(attr) From 8bde0c5957c3c132a90d5908a024ddbd8b503081 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 24 Jun 2015 13:09:41 +0200 Subject: [PATCH 18/34] allow faillure on 35 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4ad059331..15393b706 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,8 @@ env: - TESTENV=doctesting - TESTENV=py27-cxfreeze - TESTENV=coveralls + allow_failures: + - TESTENV=py35 script: tox --recreate -i ALL=https://devpi.net/hpk/dev/ -e $TESTENV notifications: From 195afa0733cdd3a7b8af9a499c7b8e362321b7a9 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 24 Jun 2015 13:16:54 +0200 Subject: [PATCH 19/34] try isntall 35 on tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2e1a15b8b..cf8d577ed 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ minversion=2.0 distshare={homedir}/.tox/distshare envlist= - flakes,py26,py27,py33,py34,pypy, + flakes,py26,py27,py33,py34,py35,pypy, {py27,py34}-{pexpect,xdist,trial}, py27-nobyte,doctesting,py27-cxfreeze From ec5286ea8c2bb7b5c63a567a759fd32f3bc18b45 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 24 Jun 2015 17:34:09 +0200 Subject: [PATCH 20/34] nigh --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 15393b706..5fedc3cd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ sudo: false language: python +python: + - 'nightly' # command to install dependencies install: "pip install -U tox" # # command to run tests From a4dbb27faba91fe128bb720dad547a06c62fc164 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 24 Jun 2015 17:51:59 +0200 Subject: [PATCH 21/34] a test --- _pytest/assertion/newinterpret.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py index d8b27784d..c8d999d48 100644 --- a/_pytest/assertion/newinterpret.py +++ b/_pytest/assertion/newinterpret.py @@ -243,13 +243,13 @@ class DebugInterpreter(ast.NodeVisitor): keyword_source = "%s=%%s" % (keyword.arg) arguments.append(keyword_source % (arg_name,)) arg_explanations.append(keyword_source % (arg_explanation,)) - if call.starargs: + if sys.version_info <= (3,4) and call.starargs: arg_explanation, arg_result = self.visit(call.starargs) arg_name = "__exprinfo_star" ns[arg_name] = arg_result arguments.append("*%s" % (arg_name,)) arg_explanations.append("*%s" % (arg_explanation,)) - if call.kwargs: + if sys.version_info <= (3,4) and call.kwargs: arg_explanation, arg_result = self.visit(call.kwargs) arg_name = "__exprinfo_kwds" ns[arg_name] = arg_result From fad569ae1b469d76022ae627d458f8ec6a18e754 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 24 Jun 2015 21:53:52 +0200 Subject: [PATCH 22/34] simplify + fix --- _pytest/assertion/newinterpret.py | 20 +++++++++++++------- _pytest/assertion/rewrite.py | 12 ++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py index c8d999d48..20dd8ed5e 100644 --- a/_pytest/assertion/newinterpret.py +++ b/_pytest/assertion/newinterpret.py @@ -238,18 +238,24 @@ class DebugInterpreter(ast.NodeVisitor): arg_explanations.append(arg_explanation) for keyword in call.keywords: arg_explanation, arg_result = self.visit(keyword.value) - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - keyword_source = "%s=%%s" % (keyword.arg) - arguments.append(keyword_source % (arg_name,)) - arg_explanations.append(keyword_source % (arg_explanation,)) - if sys.version_info <= (3,4) and call.starargs: + if keyword.arg: + arg_name = "__exprinfo_%s" % (len(ns),) + ns[arg_name] = arg_result + keyword_source = "%s=%%s" % (keyword.arg) + arguments.append(keyword_source % (arg_name,)) + arg_explanations.append(keyword_source % (arg_explanation,)) + else: # starargs in 3.5+ + arg_name = "__exprinfo_star" + ns[arg_name] = arg_result + arguments.append("*%s" % (arg_name,)) + arg_explanations.append("*%s" % (arg_explanation,)) + if getattr(call, 'starargs', None): # no starargs in 3.5 arg_explanation, arg_result = self.visit(call.starargs) arg_name = "__exprinfo_star" ns[arg_name] = arg_result arguments.append("*%s" % (arg_name,)) arg_explanations.append("*%s" % (arg_explanation,)) - if sys.version_info <= (3,4) and call.kwargs: + if call.kwargs: arg_explanation, arg_result = self.visit(call.kwargs) arg_name = "__exprinfo_kwds" ns[arg_name] = arg_result diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 29d18d813..5e76acd1e 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -768,22 +768,18 @@ class AssertionRewriter(ast.NodeVisitor): new_args = [] new_kwargs = [] for arg in call.args: + res, expl = self.visit(arg) if type(arg) is ast.Starred: - new_star, expl = self.visit(arg) arg_expls.append("*" + expl) - new_args.append(new_star) else: - res, expl = self.visit(arg) - new_args.append(res) arg_expls.append(expl) + new_args.append(res) for keyword in call.keywords: + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) if keyword.arg: - res, expl = self.visit(keyword.value) - new_kwargs.append(ast.keyword(keyword.arg, res)) arg_expls.append(keyword.arg + "=" + expl) else: ## **args have `arg` keywords with an .arg of None - res, expl = self.visit(keyword.value) - new_kwargs.append(ast.keyword(keyword.arg, res)) arg_expls.append("**" + expl) expl = "%s(%s)" % (func_expl, ', '.join(arg_expls)) From 728d8fbdc5e9b2a12ea3678792f3707de4ff6357 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 24 Jun 2015 22:06:34 +0200 Subject: [PATCH 23/34] generify --- _pytest/assertion/newinterpret.py | 38 +++++++++++++++++++------------ 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py index 20dd8ed5e..ff20082dc 100644 --- a/_pytest/assertion/newinterpret.py +++ b/_pytest/assertion/newinterpret.py @@ -232,30 +232,38 @@ class DebugInterpreter(ast.NodeVisitor): arguments = [] for arg in call.args: arg_explanation, arg_result = self.visit(arg) - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - arguments.append(arg_name) - arg_explanations.append(arg_explanation) - for keyword in call.keywords: - arg_explanation, arg_result = self.visit(keyword.value) - if keyword.arg: - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - keyword_source = "%s=%%s" % (keyword.arg) - arguments.append(keyword_source % (arg_name,)) - arg_explanations.append(keyword_source % (arg_explanation,)) - else: # starargs in 3.5+ + if type(arg) is ast.Starred: arg_name = "__exprinfo_star" ns[arg_name] = arg_result arguments.append("*%s" % (arg_name,)) arg_explanations.append("*%s" % (arg_explanation,)) - if getattr(call, 'starargs', None): # no starargs in 3.5 + else: + arg_name = "__exprinfo_%s" % (len(ns),) + ns[arg_name] = arg_result + arguments.append(arg_name) + arg_explanations.append(arg_explanation) + for keyword in call.keywords: + arg_explanation, arg_result = self.visit(keyword.value) + if keyword.arg: + arg_name = "__exprinfo_%s" % (len(ns),) + keyword_source = "%s=%%s" % (keyword.arg) + arguments.append(keyword_source % (arg_name,)) + arg_explanations.append(keyword_source % (arg_explanation,)) + else: + arg_name = "__exprinfo_kwds" + arguments.append("**%s" % (arg_name,)) + arg_explanations.append("**%s" % (arg_explanation,)) + + ns[arg_name] = arg_result + + if getattr(call, 'starargs', None): arg_explanation, arg_result = self.visit(call.starargs) arg_name = "__exprinfo_star" ns[arg_name] = arg_result arguments.append("*%s" % (arg_name,)) arg_explanations.append("*%s" % (arg_explanation,)) - if call.kwargs: + + if getattr(call, 'kwargs', None): arg_explanation, arg_result = self.visit(call.kwargs) arg_name = "__exprinfo_kwds" ns[arg_name] = arg_result From 570c4cc55a9733024bdd19284cc61c2a897b7712 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Jul 2015 14:07:43 -0700 Subject: [PATCH 24/34] No Starred node type on Python 2 --- _pytest/assertion/newinterpret.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py index ff20082dc..d8e741162 100644 --- a/_pytest/assertion/newinterpret.py +++ b/_pytest/assertion/newinterpret.py @@ -33,6 +33,12 @@ else: def _is_ast_stmt(node): return isinstance(node, ast.stmt) +try: + _Starred = ast.Starred +except AttributeError: + # Python 2. Define a dummy class so isinstance() will always be False. + class _Starred(object): pass + class Failure(Exception): """Error found while interpreting AST.""" @@ -232,7 +238,7 @@ class DebugInterpreter(ast.NodeVisitor): arguments = [] for arg in call.args: arg_explanation, arg_result = self.visit(arg) - if type(arg) is ast.Starred: + if isinstance(arg, _Starred): arg_name = "__exprinfo_star" ns[arg_name] = arg_result arguments.append("*%s" % (arg_name,)) From 26e7532756f5e6ad1c0c7dc42bb6801467ce8215 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Jul 2015 14:28:43 -0700 Subject: [PATCH 25/34] Move Interrupted exception class out of Session --- _pytest/main.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index f70e06d56..6af4dc1ca 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -502,10 +502,12 @@ class Item(Node): class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ +class Interrupted(KeyboardInterrupt): + """ signals an interrupted test run. """ + __module__ = 'builtins' # for py3 + class Session(FSCollector): - class Interrupted(KeyboardInterrupt): - """ signals an interrupted test run. """ - __module__ = 'builtins' # for py3 + Interrupted = Interrupted def __init__(self, config): FSCollector.__init__(self, config.rootdir, parent=None, From d4789f1ac4d92f577c657f3b3ba13865d37c5007 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Jul 2015 15:31:35 -0700 Subject: [PATCH 26/34] Fix AST rewriting with starred expressions in function calls --- _pytest/assertion/rewrite.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 5e76acd1e..ff41e1129 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -769,10 +769,7 @@ class AssertionRewriter(ast.NodeVisitor): new_kwargs = [] for arg in call.args: res, expl = self.visit(arg) - if type(arg) is ast.Starred: - arg_expls.append("*" + expl) - else: - arg_expls.append(expl) + arg_expls.append(expl) new_args.append(res) for keyword in call.keywords: res, expl = self.visit(keyword.value) @@ -789,6 +786,11 @@ class AssertionRewriter(ast.NodeVisitor): outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) return res, outer_expl + def visit_Starred(self, starred): + # From Python 3.5, a Starred node can appear in a function call + res, expl = self.visit(starred.value) + return starred, '*' + expl + def visit_Call_legacy(self, call): """ visit `ast.Call nodes on 3.4 and below` From 8a0867c580f2a51794b30ae22d792c9ec6628067 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Jul 2015 15:49:15 -0700 Subject: [PATCH 27/34] Try running flakes tests with Python 3.4 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index cf8d577ed..5988a12f7 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,7 @@ deps= commands= py.test --genscript=pytest1 [testenv:flakes] +basepython = python3.4 deps = pytest-flakes>=0.2 commands = py.test --flakes -m flakes _pytest testing From 15497dcd77de9eefb63f71680d92d95f2b051e29 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Jul 2015 15:52:28 -0700 Subject: [PATCH 28/34] OK, try running flakes with 2.7 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5988a12f7..0b262e562 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ deps= commands= py.test --genscript=pytest1 [testenv:flakes] -basepython = python3.4 +basepython = python2.7 deps = pytest-flakes>=0.2 commands = py.test --flakes -m flakes _pytest testing From 08432c3e97b8929b99ec292a3ac8cf2fdc2bd626 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Jul 2015 15:58:42 -0700 Subject: [PATCH 29/34] No more failures --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5fedc3cd0..79be84547 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,8 +29,6 @@ env: - TESTENV=doctesting - TESTENV=py27-cxfreeze - TESTENV=coveralls - allow_failures: - - TESTENV=py35 script: tox --recreate -i ALL=https://devpi.net/hpk/dev/ -e $TESTENV notifications: From 6719a818e74d095f8b42d052d4ec569aeb7a0197 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Jul 2015 16:03:14 -0700 Subject: [PATCH 30/34] Match .travis.yml env list to tox envs Conflicts: .travis.yml --- .travis.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 79be84547..d2e8e99e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,19 +10,18 @@ env: - TESTENV=flakes - TESTENV=py26 - TESTENV=py27 + - TESTENV=py33 - TESTENV=py34 - TESTENV=py35 - TESTENV=pypy - TESTENV=py27-pexpect - - TESTENV=py33-pexpect + - TESTENV=py34-pexpect - TESTENV=py27-nobyte - - TESTENV=py33 - TESTENV=py27-xdist - - TESTENV=py33-xdist - - TESTENV=py27 + - TESTENV=py34-xdist - TESTENV=py27-trial - TESTENV=py33 - - TESTENV=py33-trial + - TESTENV=py34-trial # inprocess tests by default were introduced in 2.8 only; # this TESTENV should be enabled when merged back to master #- TESTENV=py27-subprocess From e227950b0679d9d45aa6076e886130142eaf5150 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 16 Jul 2015 11:04:36 -0700 Subject: [PATCH 31/34] Style fix --- _pytest/assertion/rewrite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index ff41e1129..6dbbd4f49 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -37,8 +37,8 @@ ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3 if sys.version_info >= (3,5): ast_Call = ast.Call -else : - ast_Call = lambda a,b,c : ast.Call(a, b, c, None, None) +else: + ast_Call = lambda a,b,c: ast.Call(a, b, c, None, None) class AssertionRewritingHook(object): From da1d5712cf3771de8ebe9fdfffb4c8131329aad7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 18 Jul 2015 14:15:07 -0300 Subject: [PATCH 32/34] Fix broken links Fix #857 --- doc/en/faq.txt | 7 ++++--- doc/en/links.inc | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/en/faq.txt b/doc/en/faq.txt index 2f7bd17d2..88ae460e5 100644 --- a/doc/en/faq.txt +++ b/doc/en/faq.txt @@ -106,15 +106,16 @@ Is using pytest fixtures versus xUnit setup a style question? For simple applications and for people experienced with nose_ or unittest-style test setup using `xUnit style setup`_ probably feels natural. For larger test suites, parametrized testing -or setup of complex test resources using funcargs_ may feel more natural. -Moreover, funcargs are ideal for writing advanced test support -code (like e.g. the monkeypatch_, the tmpdir_ or capture_ funcargs) +or setup of complex test resources using fixtures_ may feel more natural. +Moreover, fixtures are ideal for writing advanced test support +code (like e.g. the monkeypatch_, the tmpdir_ or capture_ fixtures) because the support code can register setup/teardown functions in a managed class/module/function scope. .. _monkeypatch: monkeypatch.html .. _tmpdir: tmpdir.html .. _capture: capture.html +.. _fixtures: fixture.html .. _`why pytest_pyfuncarg__ methods?`: diff --git a/doc/en/links.inc b/doc/en/links.inc index 76a2bfd32..3d7863751 100644 --- a/doc/en/links.inc +++ b/doc/en/links.inc @@ -6,7 +6,7 @@ .. _`pytest_nose`: plugin/nose.html .. _`reStructured Text`: http://docutils.sourceforge.net .. _`Python debugger`: http://docs.python.org/lib/module-pdb.html -.. _nose: http://somethingaboutorange.com/mrl/projects/nose/ +.. _nose: https://nose.readthedocs.org/en/latest/ .. _pytest: http://pypi.python.org/pypi/pytest .. _mercurial: http://mercurial.selenic.com/wiki/ .. _`setuptools`: http://pypi.python.org/pypi/setuptools From 23aaa8a62c0812ba3e608abd79a385add8326f0f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 18 Jul 2015 14:46:36 -0300 Subject: [PATCH 33/34] Allow py35 to fail on Travis until it is properly supported (fix2) Conflicts: .travis.yml --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index d2e8e99e9..88c45516a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,10 @@ env: - TESTENV=doctesting - TESTENV=py27-cxfreeze - TESTENV=coveralls +matrix: + allow_failures: + # py35 is currently broken on travis, see #744 + - env: TESTENV=py35 script: tox --recreate -i ALL=https://devpi.net/hpk/dev/ -e $TESTENV notifications: From 953916df49fbb717da908845632a66753f01133d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 18 Jul 2015 17:16:27 -0300 Subject: [PATCH 34/34] Report correct reason when using multiple skip/xfail markers --- CHANGELOG | 4 ++++ _pytest/skipping.py | 40 ++++++++++++++++++++++++++-------------- testing/test_skipping.py | 20 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 03c804514..8bff69a53 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,10 @@ fixtures declared on the first one. Thanks Florian Bruhin for reporting and Bruno Oliveira for the PR. +- fix issue863: skipped tests now report the correct reason when a skip/xfail + condition is met when using multiple markers. + Thanks Raphael Pierzina for reporting and Bruno Oliveira for the PR. + - optimized tmpdir fixture initialization, which should make test sessions faster (specially when using pytest-xdist). The only visible effect is that now pytest uses a subdirectory in the $TEMP directory for all diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 5f31166aa..2f931d879 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -98,24 +98,36 @@ class MarkEvaluator: return d def _istrue(self): + if hasattr(self, 'result'): + return self.result if self.holder: d = self._getglobals() if self.holder.args: self.result = False - for expr in self.holder.args: - self.expr = expr - if isinstance(expr, py.builtin._basestring): - result = cached_eval(self.item.config, expr, d) - else: - if self.get("reason") is None: - # XXX better be checked at collection time - pytest.fail("you need to specify reason=STRING " - "when using booleans as conditions.") - result = bool(expr) - if result: - self.result = True + # "holder" might be a MarkInfo or a MarkDecorator; only + # MarkInfo keeps track of all parameters it received in an + # _arglist attribute + if hasattr(self.holder, '_arglist'): + arglist = self.holder._arglist + else: + arglist = [(self.holder.args, self.holder.kwargs)] + for args, kwargs in arglist: + for expr in args: self.expr = expr - break + if isinstance(expr, py.builtin._basestring): + result = cached_eval(self.item.config, expr, d) + else: + if "reason" not in kwargs: + # XXX better be checked at collection time + msg = "you need to specify reason=STRING " \ + "when using booleans as conditions." + pytest.fail(msg) + result = bool(expr) + if result: + self.result = True + self.reason = kwargs.get('reason', None) + self.expr = expr + return self.result else: self.result = True return getattr(self, 'result', False) @@ -124,7 +136,7 @@ class MarkEvaluator: return self.holder.kwargs.get(attr, default) def getexplanation(self): - expl = self.get('reason', None) + expl = getattr(self, 'reason', None) or self.get('reason', None) if not expl: if not hasattr(self, 'expr'): return "" diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 00f6833e6..6e827cb46 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -409,6 +409,26 @@ class TestSkipif: ]) assert result.ret == 0 + @pytest.mark.parametrize('marker, msg1, msg2', [ + ('skipif', 'SKIP', 'skipped'), + ('xfail', 'XPASS', 'xpassed'), + ]) + def test_skipif_reporting_multiple(self, testdir, marker, msg1, msg2): + testdir.makepyfile(test_foo=""" + import pytest + @pytest.mark.{marker}(False, reason='first_condition') + @pytest.mark.{marker}(True, reason='second_condition') + def test_foobar(): + assert 1 + """.format(marker=marker)) + result = testdir.runpytest('-s', '-rsxX') + result.stdout.fnmatch_lines([ + "*{msg1}*test_foo.py*second_condition*".format(msg1=msg1), + "*1 {msg2}*".format(msg2=msg2), + ]) + assert result.ret == 0 + + def test_skip_not_report_default(testdir): p = testdir.makepyfile(test_one=""" import pytest