diff --git a/AUTHORS b/AUTHORS index b19a850fb..8b07e8bf2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -44,6 +44,7 @@ David Mohr David Vierra Diego Russo Dmitry Dygalo +Duncan Betts Edison Gustavo Muenz Edoardo Batini Eduardo Schettino @@ -82,6 +83,7 @@ Kevin Cox Lee Kamentsky Lev Maximov Lukas Bednar +Luke Murphy Maciek Fijalkowski Maho Marc Schlaich @@ -102,6 +104,8 @@ Michael Birtwell Michael Droettboom Michael Seifert Mike Lundy +Ned Batchelder +Neven Mundar Nicolas Delaby Oleg Pidsadnyi Oliver Bestwalter diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f4c22c43f..dd5c68123 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,16 +45,44 @@ Changes ========== +* Add hint to error message hinting possible missing ``__init__.py`` (`#478`_). Thanks `@DuncanBetts`_. + + +* Provide ``:ref:`` targets for ``recwarn.rst`` so we can use intersphinx referencing. + Thanks to `@dupuy`_ for the report and `@lwm`_ for the PR. + +* Using ``item.Function``, ``item.Module``, etc., is now issuing deprecation warnings, prefer + ``pytest.Function``, ``pytest.Module``, etc., instead (`#2034`_). + Thanks `@nmundar`_ for the PR. + +* An error message is now displayed if ``--confcutdir`` is not a valid directory, avoiding + subtle bugs (`#2078`_). + Thanks `@nicoddemus`_ for the PR. + +* Fix error message using ``approx`` with complex numbers (`#2082`_). + Thanks `@adler-j`_ for the report and `@nicoddemus`_ for the PR. + +* + +* Cope gracefully with a .pyc file with no matching .py file (`#2038`_). Thanks + `@nedbat`_. * * -* +.. _@dupuy: https://bitbucket.org/dupuy/ +.. _@lwm: https://github.com/lwm +.. _@adler-j: https://github.com/adler-j +.. _@DuncanBetts: https://github.com/DuncanBetts +.. _@nedbat: https://github.com/nedbat +.. _@nmundar: https://github.com/nmundar -* - -* +.. _#478: https://github.com/pytest-dev/pytest/issues/478 +.. _#2034: https://github.com/pytest-dev/pytest/issues/2034 +.. _#2038: https://github.com/pytest-dev/pytest/issues/2038 +.. _#2078: https://github.com/pytest-dev/pytest/issues/2078 +.. _#2082: https://github.com/pytest-dev/pytest/issues/2082 3.0.4 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 19b31c7f4..71dc04d91 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -79,6 +79,16 @@ Pytest could always use more documentation. What exactly is needed? You can also edit documentation files directly in the GitHub web interface, without using a local copy. This can be convenient for small fixes. +.. note:: + Build the documentation locally with the following command: + + .. code:: bash + + $ tox -e docs + + The built documentation should be available in the ``doc/en/_build/``. + + Where 'en' refers to the documentation language. .. _submitplugin: diff --git a/README.rst b/README.rst index 5b2d329e0..d5650af65 100644 --- a/README.rst +++ b/README.rst @@ -24,31 +24,31 @@ An example of a simple test: .. code-block:: python # content of test_sample.py - def func(x): + def inc(x): return x + 1 def test_answer(): - assert func(3) == 5 + assert inc(3) == 5 To execute it:: $ pytest - ======= test session starts ======== + ============================= test session starts ============================= collected 1 items test_sample.py F - ======= FAILURES ======== - _______ test_answer ________ + ================================== FAILURES =================================== + _________________________________ test_answer _________________________________ def test_answer(): - > assert func(3) == 5 + > assert inc(3) == 5 E assert 4 == 5 - E + where 4 = func(3) + E + where 4 = inc(3) test_sample.py:5: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================== 1 failed in 0.04 seconds =========================== Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started `_ for more examples. diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index c2098936d..9ad08391c 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -80,7 +80,12 @@ class AssertionRewritingHook(object): tp = desc[2] if tp == imp.PY_COMPILED: if hasattr(imp, "source_from_cache"): - fn = imp.source_from_cache(fn) + try: + fn = imp.source_from_cache(fn) + except ValueError: + # Python 3 doesn't like orphaned but still-importable + # .pyc files. + fn = fn[:-1] else: fn = fn[:-1] elif tp != imp.PY_SOURCE: diff --git a/_pytest/config.py b/_pytest/config.py index ab5e1b994..61123f6ac 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -996,6 +996,7 @@ class Config(object): "(are you using python -O?)\n") def _preparse(self, args, addopts=True): + import pytest self._initini(args) if addopts: args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args @@ -1007,7 +1008,10 @@ class Config(object): self.pluginmanager.load_setuptools_entrypoints(entrypoint_name) self.pluginmanager.consider_env() self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy()) - if self.known_args_namespace.confcutdir is None and self.inifile: + confcutdir = self.known_args_namespace.confcutdir + if confcutdir and not os.path.isdir(confcutdir): + raise pytest.UsageError('--confcutdir must be a directory, given: {0}'.format(confcutdir)) + if confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir try: diff --git a/_pytest/main.py b/_pytest/main.py index 451355ef3..91f7bea56 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -190,7 +190,9 @@ class FSHookProxy: def compatproperty(name): def fget(self): - # deprecated - use pytest.name + import warnings + warnings.warn("This usage is deprecated, please use pytest.{0} instead".format(name), + PendingDeprecationWarning, stacklevel=2) return getattr(pytest, name) return property(fget) @@ -700,10 +702,9 @@ class Session(FSCollector): path = self.config.invocation_dir.join(relpath, abs=True) if not path.check(): if self.config.option.pyargs: - msg = "file or package not found: " + raise pytest.UsageError("file or package not found: " + arg + " (missing __init__.py?)") else: - msg = "file not found: " - raise pytest.UsageError(msg + arg) + raise pytest.UsageError("file not found: " + arg) parts[0] = path return parts diff --git a/_pytest/python.py b/_pytest/python.py index b18c00897..3f01d22ad 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1414,6 +1414,9 @@ class ApproxNonIterable(object): self.rel = rel def __repr__(self): + if isinstance(self.expected, complex): + return str(self.expected) + # Infinities aren't compared using tolerances, so don't show a # tolerance. if math.isinf(self.expected): diff --git a/appveyor.yml b/appveyor.yml index 7192ec06f..a42aa16dc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,8 @@ environment: # https://www.appveyor.com/docs/build-configuration#secure-variables matrix: + # coveralls is not in the default env list + - TOXENV: "coveralls" # note: please use "tox --listenvs" to populate the build matrix below - TOXENV: "linting" - TOXENV: "py26" @@ -29,14 +31,11 @@ install: - echo Installed Pythons - dir c:\Python* - - if "%TOXENV%" == "pypy" scripts\install-pypy.bat + - if "%TOXENV%" == "pypy" call scripts\install-pypy.bat - C:\Python35\python -m pip install tox build: false # Not a C# project, build stuff at the test step instead. test_script: - - C:\Python35\python -m tox - # coveralls is not in tox's envlist, plus for PRs the secure variable - # is not defined so we have to check for it - - if defined COVERALLS_REPO_TOKEN C:\Python35\python -m tox -e coveralls + - call scripts\call-tox.bat diff --git a/doc/en/goodpractices.rst b/doc/en/goodpractices.rst index 2a5d4d7c8..d1e3e66fd 100644 --- a/doc/en/goodpractices.rst +++ b/doc/en/goodpractices.rst @@ -16,10 +16,12 @@ Conventions for Python test discovery * If no arguments are specified then collection starts from :confval:`testpaths` (if configured) or the current directory. Alternatively, command line arguments can be used in any combination of directories, file names or node ids. -* recurse into directories, unless they match :confval:`norecursedirs` -* ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_. -* ``Test`` prefixed test classes (without an ``__init__`` method) -* ``test_`` prefixed test functions or methods are test items +* Recurse into directories, unless they match :confval:`norecursedirs`. +* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_. +* From those files, collect test items: + + * ``test_`` prefixed test functions or methods outside of class + * ``test_`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method) For examples of how to customize your test discovery :doc:`example/pythoncollection`. diff --git a/doc/en/index.rst b/doc/en/index.rst index aadabf222..67b13d3e3 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -14,33 +14,31 @@ An example of a simple test: .. code-block:: python # content of test_sample.py - def func(x): + def inc(x): return x + 1 def test_answer(): - assert func(3) == 5 + assert inc(3) == 5 To execute it:: $ pytest - ======= test session starts ======== - platform linux -- Python 3.5.2, pytest-3.0.4, py-1.4.31, pluggy-0.4.0 - rootdir: $REGENDOC_TMPDIR, inifile: + ============================= test session starts ============================= collected 1 items - + test_sample.py F - - ======= FAILURES ======== - _______ test_answer ________ - + + ================================== FAILURES =================================== + _________________________________ test_answer _________________________________ + def test_answer(): - > assert func(3) == 5 + > assert inc(3) == 5 E assert 4 == 5 - E + where 4 = func(3) - + E + where 4 = inc(3) + test_sample.py:5: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================== 1 failed in 0.04 seconds =========================== Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See :ref:`Getting Started ` for more examples. diff --git a/doc/en/recwarn.rst b/doc/en/recwarn.rst index 3eb333912..823ba945b 100644 --- a/doc/en/recwarn.rst +++ b/doc/en/recwarn.rst @@ -1,8 +1,12 @@ +.. _`asserting warnings`: + .. _assertwarnings: Asserting Warnings ===================================================== +.. _`asserting warnings with the warns function`: + .. _warns: Asserting warnings with the warns function @@ -46,6 +50,8 @@ Alternatively, you can examine raised warnings in detail using the ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated differently; see :ref:`ensuring_function_triggers`. +.. _`recording warnings`: + .. _recwarn: Recording warnings @@ -99,6 +105,8 @@ class of the warning. The ``message`` is the warning itself; calling ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated differently; see :ref:`ensuring_function_triggers`. +.. _`ensuring a function triggers a deprecation warning`: + .. _ensuring_function_triggers: Ensuring a function triggers a deprecation warning diff --git a/scripts/call-tox.bat b/scripts/call-tox.bat new file mode 100644 index 000000000..3ca9eb6d7 --- /dev/null +++ b/scripts/call-tox.bat @@ -0,0 +1,8 @@ +REM skip "coveralls" run in PRs or forks +if "%TOXENV%" == "coveralls" ( + if not defined COVERALLS_REPO_TOKEN ( + echo skipping coveralls run because COVERALLS_REPO_TOKEN is not defined + exit /b 0 + ) +) +C:\Python35\python -m tox diff --git a/testing/python/approx.py b/testing/python/approx.py index 2720c573f..d6bb1f9cf 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -28,6 +28,7 @@ class TestApprox: print(approx(inf)) print(approx(1.0, rel=nan)) print(approx(1.0, rel=inf)) + print(approx(1.0j, rel=inf)) def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 483d80b3d..5f36d4a6b 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1,7 +1,10 @@ +import glob import os +import py_compile import stat import sys import zipfile + import py import pytest @@ -573,6 +576,31 @@ def test_rewritten(): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") assert testdir.runpytest_subprocess().ret == 0 + def test_orphaned_pyc_file(self, testdir): + if sys.version_info < (3, 0) and hasattr(sys, 'pypy_version_info'): + pytest.skip("pypy2 doesn't run orphaned pyc files") + + testdir.makepyfile(""" + import orphan + def test_it(): + assert orphan.value == 17 + """) + testdir.makepyfile(orphan=""" + value = 17 + """) + py_compile.compile("orphan.py") + os.remove("orphan.py") + + # Python 3 puts the .pyc files in a __pycache__ directory, and will + # not import from there without source. It will import a .pyc from + # the source location though. + if not os.path.exists("orphan.pyc"): + pycs = glob.glob("__pycache__/orphan.*.pyc") + assert len(pycs) == 1 + os.rename(pycs[0], "orphan.pyc") + + assert testdir.runpytest().ret == 0 + @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): testdir.makepyfile(""" diff --git a/testing/test_config.py b/testing/test_config.py index 75a806c4a..1567dd27c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -294,6 +294,15 @@ class TestConfigAPI: assert len(l) == 2 assert l == ["456", "123"] + def test_confcutdir_check_isdir(self, testdir): + """Give an error if --confcutdir is not a valid directory (#2078)""" + with pytest.raises(pytest.UsageError): + testdir.parseconfig('--confcutdir', testdir.tmpdir.join('file').ensure(file=1)) + with pytest.raises(pytest.UsageError): + testdir.parseconfig('--confcutdir', testdir.tmpdir.join('inexistant')) + config = testdir.parseconfig('--confcutdir', testdir.tmpdir.join('dir').ensure(dir=1)) + assert config.getoption('confcutdir') == str(testdir.tmpdir.join('dir')) + class TestConfigFromdictargs: def test_basic_behavior(self): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index f4455a081..d9d06651d 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -249,6 +249,18 @@ class TestPython: snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello25", ) + def test_mark_skip_doesnt_capture_output(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skip(reason="foo") + def test_skip(): + print("bar!") + """) + result, dom = runandparse(testdir) + assert result.ret == 0 + node_xml = dom.find_first_by_tag("testsuite").toxml() + assert "bar!" not in node_xml + def test_classname_instance(self, testdir): testdir.makepyfile(""" class TestClass: diff --git a/tox.ini b/tox.ini index c2f866f26..f3494e8be 100644 --- a/tox.ini +++ b/tox.ini @@ -117,7 +117,8 @@ commands= basepython = python usedevelop=True skipsdist=True -deps=PyYAML +deps= + PyYAML commands= pytest -rfsxX doc/en pytest --doctest-modules {toxinidir}/_pytest