From 24c95c78e7f332c8d466e86eafea44589a029bee Mon Sep 17 00:00:00 2001 From: Evan Kepner Date: Mon, 27 May 2019 20:54:42 -0400 Subject: [PATCH 001/109] add updated monkeypatch examples --- changelog/5315.doc.rst | 1 + doc/en/monkeypatch.rst | 316 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 298 insertions(+), 19 deletions(-) create mode 100644 changelog/5315.doc.rst diff --git a/changelog/5315.doc.rst b/changelog/5315.doc.rst new file mode 100644 index 00000000000..4cb46358308 --- /dev/null +++ b/changelog/5315.doc.rst @@ -0,0 +1 @@ +Expand docs on mocking classes and dictionaries with ``monkeypatch``. diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index c9304e0fb70..a165f077b5a 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -8,46 +8,217 @@ Sometimes tests need to invoke functionality which depends on global settings or which invokes code which cannot be easily tested such as network access. The ``monkeypatch`` fixture helps you to safely set/delete an attribute, dictionary item or -environment variable or to modify ``sys.path`` for importing. +environment variable, or to modify ``sys.path`` for importing. + +The ``monkeypatch`` fixture provides these helper methods for safely patching and mocking +functionality in tests: + +.. code-block:: python + + monkeypatch.setattr(obj, name, value, raising=True) + monkeypatch.delattr(obj, name, raising=True) + monkeypatch.setitem(mapping, name, value) + monkeypatch.delitem(obj, name, raising=True) + monkeypatch.setenv(name, value, prepend=False) + monkeypatch.delenv(name, raising=True) + monkeypatch.syspath_prepend(path) + monkeypatch.chdir(path) + +All modifications will be undone after the requesting +test function or fixture has finished. The ``raising`` +parameter determines if a ``KeyError`` or ``AttributeError`` +will be raised if the target of the set/deletion operation does not exist. + +Consider the following scenarios: + +1. Modifying the behavior of a function or the property of a class for a test e.g. +there is an API call or database connection you will not make for a test but you know +what the expected output should be. Use :py:meth:`monkeypatch.setattr` to patch the +function or property with your desired testing behavior. This can include your own functions. +Use :py:meth:`monkeypatch.delattr` to remove the function or property for the test. + +2. Modifying the values of dictionaries e.g. you have a global configuration that +you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem` to patch the +dictionary for the test. :py:meth:`monkeypatch.delitem` can be used to remove items. + +3. Modifying environment variables for a test e.g. to test program behavior if an +environment variable is missing, or to set multiple values to a known variable. +:py:meth:`monkeypatch.setenv` and :py:meth:`monkeypatch.delenv` can be used for +these patches. + +4. Use :py:meth:`monkeypatch.syspath_prepend` to modify the system ``$PATH`` safely, and +:py:meth:`monkeypatch.chdir` to change the context of the current working directory +during a test. + See the `monkeypatch blog post`_ for some introduction material and a discussion of its motivation. .. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ - Simple example: monkeypatching functions ---------------------------------------- -If you want to pretend that ``os.expanduser`` returns a certain -directory, you can use the :py:meth:`monkeypatch.setattr` method to -patch this function before calling into a function which uses it:: +Consider a scenario where you are working with user directories. In the context of +testing, you do not want your test to depend on the running user. ``monkeypatch`` +can be used to patch functions dependent on the user to always return a +specific value. + +In this example, :py:meth:`monkeypatch.setattr` is used to patch ``os.path.expanduser`` +so that the known testing string ``"/abc"`` is always used when the test is run. +This removes any dependency on the running user for testing purposes. +:py:meth:`monkeypatch.setattr` must be called before the function which will use +the patched function is called. +After the test function finishes the ``os.path.expanduser`` modification will be undone. - # content of test_module.py +.. code-block:: python + + # contents of test_module.py with source code and the test + # os.path is imported for reference in monkeypatch.setattr() import os.path - def getssh(): # pseudo application code - return os.path.join(os.path.expanduser("~admin"), '.ssh') - def test_mytest(monkeypatch): + + def getssh(): + """Simple function to return expanded homedir ssh path.""" + return os.path.expanduser("~/.ssh") + + + def test_getssh(monkeypatch): + # mocked return function to replace os.path.expanduser + # given a path, always return '/abc' def mockreturn(path): - return '/abc' - monkeypatch.setattr(os.path, 'expanduser', mockreturn) + return "/abc" + + # Application of the monkeypatch to replace os.path.expanduser + # with the behavior of mockreturn defined above. + monkeypatch.setattr(os.path, "expanduser", mockreturn) + + # Calling getssh() will use mockreturn in place of os.path.expanduser + # for this test with the monkeypatch. x = getssh() - assert x == '/abc/.ssh' + assert x == "/abc/.ssh" + + +Monkeypatching returned objects: building mock classes +------------------------------------------------------ + +:py:meth:`monkeypatch.setattr` can be used in conjunction with classes to mock returned +objects from functions instead of values. +Imagine a simple function to take an API url and return the json response. + +.. code-block:: python + + # contents of app.py, a simple API retrieval example + import requests + + + def get_json(url): + """Takes a URL, and returns the JSON.""" + r = requests.get(url) + return r.json() + +We need to mock ``r``, the returned response object for testing purposes. +The mock of ``r`` needs a ``.json()`` method which returns a dictionary. +This can be done in our test file by defining a class to represent ``r``. + +.. code-block:: python + + # contents of test_app.py, a simple test for our API retrieval + # import requests for the purposes of monkeypatching + import requests + + # our app.py that includes the get_json() function + # this is the previous code block example + import app + + # custom class to be the mock return value + # will override the requests.Response returned from requests.get + class MockResponse: + + # mock json() method always returns a specific testing dictionary + @staticmethod + def json(): + return {"mock_key": "mock_response"} + + + def test_get_json(monkeypatch): + + # Any arguments may be passed and mock_get() will always return our + # mocked object, which only has the .json() method. + def mock_get(*args, **kwargs): + return MockResponse() + + # apply the monkeypatch for requests.get to mock_get + monkeypatch.setattr(requests, "get", mock_get) + + # app.get_json, which contains requests.get, uses the monkeypatch + result = app.get_json("https://fakeurl") + assert result["mock_key"] == "mock_response" + + +``monkeypatch`` applies the mock for ``requests.get`` with our ``mock_get`` function. +The ``mock_get`` function returns an instance of the ``MockResponse`` class, which +has a ``json()`` method defined to return a known testing dictionary and does not +require any outside API connection. + +You can build the ``MockResponse`` class with the appropriate degree of complexity for +the scenario you are testing. For instance, it could include an ``ok`` property that +always returns ``True``, or return different values from the ``json()`` mocked method +based on input strings. + +This mock can be shared across tests using a ``fixture``: + +.. code-block:: python + + # contents of test_app.py, a simple test for our API retrieval + import pytest + import requests + + # app.py that includes the get_json() function + import app + + # custom class to be the mock return value of requests.get() + class MockResponse: + @staticmethod + def json(): + return {"mock_key": "mock_response"} + + + # monkeypatched requests.get moved to a fixture + @pytest.fixture + def mock_response(monkeypatch): + """Requests.get() mocked to return {'mock_key':'mock_response'}.""" + + def mock_get(*args, **kwargs): + return MockResponse() + + monkeypatch.setattr(requests, "get", mock_get) + + + # notice our test uses the custom fixture instead of monkeypatch directly + def test_get_json(mock_response): + result = app.get_json("https://fakeurl") + assert result["mock_key"] == "mock_response" + + +Furthermore, if the mock was designed to be applied to all tests, the ``fixture`` could +be moved to a ``conftest.py`` file and use the with ``autouse=True`` option. -Here our test function monkeypatches ``os.path.expanduser`` and -then calls into a function that calls it. After the test function -finishes the ``os.path.expanduser`` modification will be undone. Global patch example: preventing "requests" from remote operations ------------------------------------------------------------------ If you want to prevent the "requests" library from performing http -requests in all your tests, you can do:: +requests in all your tests, you can do: - # content of conftest.py +.. code-block:: python + + # contents of conftest.py import pytest + + @pytest.fixture(autouse=True) def no_requests(monkeypatch): + """Remove requests.sessions.Session.request for all tests.""" monkeypatch.delattr("requests.sessions.Session.request") This autouse fixture will be executed for each test function and it @@ -85,7 +256,7 @@ Monkeypatching environment variables ------------------------------------ If you are working with environment variables you often need to safely change the values -or delete them from the system for testing purposes. ``Monkeypatch`` provides a mechanism +or delete them from the system for testing purposes. ``monkeypatch`` provides a mechanism to do this using the ``setenv`` and ``delenv`` method. Our example code to test: .. code-block:: python @@ -131,6 +302,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests: .. code-block:: python + # contents of our test file e.g. test_code.py import pytest @@ -144,7 +316,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests: monkeypatch.delenv("USER", raising=False) - # Notice the tests reference the fixtures for mocks + # notice the tests reference the fixtures for mocks def test_upper_to_lower(mock_env_user): assert get_os_user_lower() == "testinguser" @@ -154,6 +326,112 @@ This behavior can be moved into ``fixture`` structures and shared across tests: _ = get_os_user_lower() +Monkeypatching dictionaries +--------------------------- + +:py:meth:`monkeypatch.setitem` can be used to safely set the values of dictionaries +to specific values during tests. Take this simplified connection string example: + +.. code-block:: python + + # contents of app.py to generate a simple connection string + DEFAULT_CONFIG = {"user": "user1", "database": "db1"} + + + def create_connection_string(config=None): + """Creates a connection string from input or defaults.""" + config = config or DEFAULT_CONFIG + return f"User Id={config['user']}; Location={config['database']};" + +For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific values. + +.. code-block:: python + + # contents of test_app.py + # app.py with the connection string function (prior code block) + import app + + + def test_connection(monkeypatch): + + # Patch the values of DEFAULT_CONFIG to specific + # testing values only for this test. + monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user") + monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db") + + # expected result based on the mocks + expected = "User Id=test_user; Location=test_db;" + + # the test uses the monkeypatched dictionary settings + result = app.create_connection_string() + assert result == expected + +You can use the :py:meth:`monkeypatch.delitem` to remove values. + +.. code-block:: python + + # contents of test_app.py + import pytest + + # app.py with the connection string function + import app + + + def test_missing_user(monkeypatch): + + # patch the DEFAULT_CONFIG t be missing the 'user' key + monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False) + + # Key error expected because a config is not passed, and the + # default is now missing the 'user' entry. + with pytest.raises(KeyError): + _ = app.create_connection_string() + + +The modularity of fixtures gives you the flexibility to define +separate fixtures for each potential mock and reference them in the needed tests. + +.. code-block:: python + + # contents of test_app.py + import pytest + + # app.py with the connection string function + import app + + # all of the mocks are moved into separated fixtures + @pytest.fixture + def mock_test_user(monkeypatch): + """Set the DEFAULT_CONFIG user to test_user.""" + monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user") + + + @pytest.fixture + def mock_test_database(monkeypatch): + """Set the DEFAULT_CONFIG database to test_db.""" + monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db") + + + @pytest.fixture + def mock_missing_default_user(monkeypatch): + """Remove the user key from DEFAULT_CONFIG""" + monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False) + + + # tests reference only the fixture mocks that are needed + def test_connection(mock_test_user, mock_test_database): + + expected = "User Id=test_user; Location=test_db;" + + result = app.create_connection_string() + assert result == expected + + + def test_missing_user(mock_missing_default_user): + + with pytest.raises(KeyError): + _ = app.create_connection_string() + .. currentmodule:: _pytest.monkeypatch From 2dfbed11b4c2ecfeb6c7b44c4ba40fb7d000c200 Mon Sep 17 00:00:00 2001 From: Evan Kepner Date: Mon, 27 May 2019 23:23:18 -0400 Subject: [PATCH 002/109] fix path expansion example --- doc/en/monkeypatch.rst | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index a165f077b5a..8e4622982fc 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -63,40 +63,38 @@ testing, you do not want your test to depend on the running user. ``monkeypatch` can be used to patch functions dependent on the user to always return a specific value. -In this example, :py:meth:`monkeypatch.setattr` is used to patch ``os.path.expanduser`` -so that the known testing string ``"/abc"`` is always used when the test is run. +In this example, :py:meth:`monkeypatch.setattr` is used to patch ``Path.home`` +so that the known testing path ``Path("/abc")`` is always used when the test is run. This removes any dependency on the running user for testing purposes. :py:meth:`monkeypatch.setattr` must be called before the function which will use the patched function is called. -After the test function finishes the ``os.path.expanduser`` modification will be undone. +After the test function finishes the ``Path.home`` modification will be undone. .. code-block:: python # contents of test_module.py with source code and the test - # os.path is imported for reference in monkeypatch.setattr() - import os.path + from pathlib import Path def getssh(): """Simple function to return expanded homedir ssh path.""" - return os.path.expanduser("~/.ssh") + return Path.home() / ".ssh" def test_getssh(monkeypatch): - # mocked return function to replace os.path.expanduser - # given a path, always return '/abc' - def mockreturn(path): - return "/abc" + # mocked return function to replace Path.home + # always return '/abc' + def mockreturn(): + return Path("/abc") - # Application of the monkeypatch to replace os.path.expanduser + # Application of the monkeypatch to replace Path.home # with the behavior of mockreturn defined above. - monkeypatch.setattr(os.path, "expanduser", mockreturn) + monkeypatch.setattr(Path, "home", mockreturn) - # Calling getssh() will use mockreturn in place of os.path.expanduser + # Calling getssh() will use mockreturn in place of Path.home # for this test with the monkeypatch. x = getssh() - assert x == "/abc/.ssh" - + assert x == Path("/abc/.ssh") Monkeypatching returned objects: building mock classes ------------------------------------------------------ From 2125d04501e00991f39460579f6415a239a43c4f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Jun 2019 08:34:25 -0700 Subject: [PATCH 003/109] Revert "Fix all() unroll for non-generators/non-list comprehensions (#5360)" This reverts commit 733f43b02eafe2934c2e86b7d0370e25dfe95a48, reversing changes made to e4fe41ebb754b3779a1535d43119ef3492ed6dcf. --- changelog/5358.bugfix.rst | 1 - src/_pytest/assertion/rewrite.py | 14 +++----------- testing/test_assertrewrite.py | 29 ++--------------------------- 3 files changed, 5 insertions(+), 39 deletions(-) delete mode 100644 changelog/5358.bugfix.rst diff --git a/changelog/5358.bugfix.rst b/changelog/5358.bugfix.rst deleted file mode 100644 index 181da1e0ec2..00000000000 --- a/changelog/5358.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix assertion rewriting of ``all()`` calls to deal with non-generators. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 9b431b9849b..27dcb58ee34 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -903,21 +903,11 @@ def visit_BinOp(self, binop): res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) return res, explanation - @staticmethod - def _is_any_call_with_generator_or_list_comprehension(call): - """Return True if the Call node is an 'any' call with a generator or list comprehension""" - return ( - isinstance(call.func, ast.Name) - and call.func.id == "all" - and len(call.args) == 1 - and isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)) - ) - def visit_Call(self, call): """ visit `ast.Call` nodes """ - if self._is_any_call_with_generator_or_list_comprehension(call): + if isinstance(call.func, ast.Name) and call.func.id == "all": return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] @@ -944,6 +934,8 @@ def visit_Call(self, call): def _visit_all(self, call): """Special rewrite for the builtin all function, see #5062""" + if not isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)): + return gen_exp = call.args[0] assertion_module = ast.Module( body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)] diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 6720a790f50..1c309f50c4f 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -656,7 +656,7 @@ def __repr__(self): assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg - def test_unroll_all_generator(self, testdir): + def test_unroll_generator(self, testdir): testdir.makepyfile( """ def check_even(num): @@ -671,7 +671,7 @@ def test_generator(): result = testdir.runpytest() result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) - def test_unroll_all_list_comprehension(self, testdir): + def test_unroll_list_comprehension(self, testdir): testdir.makepyfile( """ def check_even(num): @@ -686,31 +686,6 @@ def test_list_comprehension(): result = testdir.runpytest() result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) - def test_unroll_all_object(self, testdir): - """all() for non generators/non list-comprehensions (#5358)""" - testdir.makepyfile( - """ - def test(): - assert all((1, 0)) - """ - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*assert False*", "*where False = all((1, 0))*"]) - - def test_unroll_all_starred(self, testdir): - """all() for non generators/non list-comprehensions (#5358)""" - testdir.makepyfile( - """ - def test(): - x = ((1, 0),) - assert all(*x) - """ - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines( - ["*assert False*", "*where False = all(*((1, 0),))*"] - ) - def test_for_loop(self, testdir): testdir.makepyfile( """ From 1b381d5277353082f626763210f418293bfe5479 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Jun 2019 08:35:56 -0700 Subject: [PATCH 004/109] Revert "Unroll calls to any #5062 (#5103)" This reverts commit 2b9ca342809a4e05b4451a074d644c14acda844c, reversing changes made to 0a57124063a4ed63f85fb8a39dc57f6d6fc5b132. --- src/_pytest/assertion/rewrite.py | 23 -------------- testing/test_assertrewrite.py | 53 -------------------------------- 2 files changed, 76 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 27dcb58ee34..ac9b85e6716 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -907,8 +907,6 @@ def visit_Call(self, call): """ visit `ast.Call` nodes """ - if isinstance(call.func, ast.Name) and call.func.id == "all": - return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] @@ -932,27 +930,6 @@ def visit_Call(self, call): outer_expl = "{}\n{{{} = {}\n}}".format(res_expl, res_expl, expl) return res, outer_expl - def _visit_all(self, call): - """Special rewrite for the builtin all function, see #5062""" - if not isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)): - return - gen_exp = call.args[0] - assertion_module = ast.Module( - body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)] - ) - AssertionRewriter(module_path=None, config=None).run(assertion_module) - for_loop = ast.For( - iter=gen_exp.generators[0].iter, - target=gen_exp.generators[0].target, - body=assertion_module.body, - orelse=[], - ) - self.statements.append(for_loop) - return ( - ast.Num(n=1), - "", - ) # Return an empty expression, all the asserts are in the for_loop - def visit_Starred(self, starred): # From Python 3.5, a Starred node can appear in a function call res, expl = self.visit(starred.value) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 1c309f50c4f..61c5b93834f 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -635,12 +635,6 @@ def __repr__(self): else: assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] - def test_unroll_expression(self): - def f(): - assert all(x == 1 for x in range(10)) - - assert "0 == 1" in getmsg(f) - def test_custom_repr_non_ascii(self): def f(): class A: @@ -656,53 +650,6 @@ def __repr__(self): assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg - def test_unroll_generator(self, testdir): - testdir.makepyfile( - """ - def check_even(num): - if num % 2 == 0: - return True - return False - - def test_generator(): - odd_list = list(range(1,9,2)) - assert all(check_even(num) for num in odd_list)""" - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) - - def test_unroll_list_comprehension(self, testdir): - testdir.makepyfile( - """ - def check_even(num): - if num % 2 == 0: - return True - return False - - def test_list_comprehension(): - odd_list = list(range(1,9,2)) - assert all([check_even(num) for num in odd_list])""" - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) - - def test_for_loop(self, testdir): - testdir.makepyfile( - """ - def check_even(num): - if num % 2 == 0: - return True - return False - - def test_for_loop(): - odd_list = list(range(1,9,2)) - for num in odd_list: - assert check_even(num) - """ - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) - class TestRewriteOnImport: def test_pycache_is_a_file(self, testdir): From 230f736fcdeca96107e58407214092d4ad9dc3b9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Jun 2019 08:38:42 -0700 Subject: [PATCH 005/109] Add changelog entries for reverting all() handling --- changelog/5370.bugfix.rst | 1 + changelog/5371.bugfix.rst | 1 + changelog/5372.bugfix.rst | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog/5370.bugfix.rst create mode 100644 changelog/5371.bugfix.rst create mode 100644 changelog/5372.bugfix.rst diff --git a/changelog/5370.bugfix.rst b/changelog/5370.bugfix.rst new file mode 100644 index 00000000000..70def0d270a --- /dev/null +++ b/changelog/5370.bugfix.rst @@ -0,0 +1 @@ +Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions. diff --git a/changelog/5371.bugfix.rst b/changelog/5371.bugfix.rst new file mode 100644 index 00000000000..46ff5c89047 --- /dev/null +++ b/changelog/5371.bugfix.rst @@ -0,0 +1 @@ +Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``. diff --git a/changelog/5372.bugfix.rst b/changelog/5372.bugfix.rst new file mode 100644 index 00000000000..e9b644db290 --- /dev/null +++ b/changelog/5372.bugfix.rst @@ -0,0 +1 @@ +Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression. From e770db4c916df202b7fd404717909fa89cf7c8eb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 3 Jun 2019 18:30:50 +0200 Subject: [PATCH 006/109] Revert "Revert "ci: Travis: add pypy3 to allowed failures temporarily"" This reverts commit a6dc283133c6ee3a3fdedacf5d363d72b58ffa88. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2f6df4d9ad0..17795861c57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -90,6 +90,9 @@ matrix: allow_failures: - python: '3.8-dev' env: TOXENV=py38-xdist + # Temporary (https://github.com/pytest-dev/pytest/pull/5334). + - env: TOXENV=pypy3-xdist + python: 'pypy3' before_script: - | From 606d728697947bf93bec94807df0f561175566d2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 3 Jun 2019 18:54:40 +0200 Subject: [PATCH 007/109] Revert "Revert "Revert "ci: Travis: add pypy3 to allowed failures temporarily""" --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 17795861c57..2f6df4d9ad0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -90,9 +90,6 @@ matrix: allow_failures: - python: '3.8-dev' env: TOXENV=py38-xdist - # Temporary (https://github.com/pytest-dev/pytest/pull/5334). - - env: TOXENV=pypy3-xdist - python: 'pypy3' before_script: - | From 1d6bbab2b044240d34c20cc634c56dd24938cfbe Mon Sep 17 00:00:00 2001 From: Pulkit Goyal <7895pulkit@gmail.com> Date: Wed, 8 May 2019 22:42:49 +0300 Subject: [PATCH 008/109] Add a new Exceptions section in documentation and document UsageError --- doc/en/reference.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index cc5fe8d8bf6..0b168eb5442 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -951,6 +951,14 @@ PYTEST_CURRENT_TEST This is not meant to be set by users, but is set by pytest internally with the name of the current test so other processes can inspect it, see :ref:`pytest current test env` for more information. +Exceptions +---------- + +UsageError +~~~~~~~~~~ + +.. autoclass:: _pytest.config.UsageError() + .. _`ini options ref`: From 9657166a22013777fb57bbc35873fcae0c42f918 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Jun 2019 12:19:42 -0700 Subject: [PATCH 009/109] Merge pull request #5379 from asottile/release-4.6.2 Preparing release version 4.6.2 --- CHANGELOG.rst | 15 +++++++++++++++ doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.2.rst | 18 ++++++++++++++++++ doc/en/example/simple.rst | 2 +- 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 doc/en/announce/release-4.6.2.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a2bf12d7ea2..715238b327b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,21 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.2 (2019-06-03) +========================= + +Bug Fixes +--------- + +- `#5370 `_: Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions. + + +- `#5371 `_: Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``. + + +- `#5372 `_: Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression. + + pytest 4.6.1 (2019-06-02) ========================= diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 2c05e2e59cb..9379ae5b1f8 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.2 release-4.6.1 release-4.6.0 release-4.5.0 diff --git a/doc/en/announce/release-4.6.2.rst b/doc/en/announce/release-4.6.2.rst new file mode 100644 index 00000000000..8526579b9e7 --- /dev/null +++ b/doc/en/announce/release-4.6.2.rst @@ -0,0 +1,18 @@ +pytest-4.6.2 +======================================= + +pytest 4.6.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index cf8298ddc2a..140f4b840f1 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -441,7 +441,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 - 0.21s call test_some_are_slow.py::test_funcslow1 + 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= From b95bb29fc25c6ad24882029ff1314c4bf6d9e370 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Jun 2019 16:31:06 -0700 Subject: [PATCH 010/109] Remove --recreate from .travis.yml Looks like this has been in the history since the beginning of time, but we should always get a blank slate anyway Noticed this in https://github.com/crsmithdev/arrow/pull/597 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2f6df4d9ad0..d6693eb685f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -105,7 +105,7 @@ before_script: export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi -script: tox --recreate +script: tox after_success: - | From 0a91e181af2451ed1254176d9bbbcf15148ff096 Mon Sep 17 00:00:00 2001 From: Dirk Thomas Date: Tue, 4 Jun 2019 10:41:58 -0700 Subject: [PATCH 011/109] fix logic if importlib_metadata.PathDistribution.files is None --- src/_pytest/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 40f37480b4c..1f6ae98f9e3 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -784,7 +784,7 @@ def _mark_plugins_for_rewrite(self, hook): str(file) for dist in importlib_metadata.distributions() if any(ep.group == "pytest11" for ep in dist.entry_points) - for file in dist.files + for file in dist.files or [] ) for name in _iter_rewritable_modules(package_files): From 898e869bcda29d692a55149d3a89d65d1038c28a Mon Sep 17 00:00:00 2001 From: Dirk Thomas Date: Tue, 4 Jun 2019 10:55:38 -0700 Subject: [PATCH 012/109] add changelog file for #5389 --- changelog/5389.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5389.bugfix.rst diff --git a/changelog/5389.bugfix.rst b/changelog/5389.bugfix.rst new file mode 100644 index 00000000000..debf0a9da9d --- /dev/null +++ b/changelog/5389.bugfix.rst @@ -0,0 +1 @@ +Fix regressions of `#5063 `__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. From 883db6a2161d4bcf8aa2ecbcab34c74f0ff011c6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Jun 2019 17:04:15 -0300 Subject: [PATCH 013/109] Add test for packages with broken metadata Related to #5389 --- testing/test_config.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/testing/test_config.py b/testing/test_config.py index 7b2a1209eaf..c3b027ab901 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -578,6 +578,29 @@ def distributions(): testdir.parseconfig() +def test_importlib_metadata_broken_distribution(testdir, monkeypatch): + """Integration test for broken distributions with 'files' metadata being None (#5389)""" + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) + + class DummyEntryPoint: + name = "mytestplugin" + group = "pytest11" + + def load(self): + return object() + + class Distribution: + version = "1.0" + files = None + entry_points = (DummyEntryPoint(),) + + def distributions(): + return (Distribution(),) + + monkeypatch.setattr(importlib_metadata, "distributions", distributions) + testdir.parseconfig() + + @pytest.mark.parametrize("block_it", [True, False]) def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it): monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) From 9349c72cacb328850fc17c5f691455dd2e4be3aa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Jun 2019 20:17:51 -0300 Subject: [PATCH 014/109] Allow pypy3 failures again Related to #5317 --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index d6693eb685f..61d4df4dfec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -90,6 +90,10 @@ matrix: allow_failures: - python: '3.8-dev' env: TOXENV=py38-xdist + # failing due to ResourceWarning exceptions: + # https://github.com/pytest-dev/pytest/issues/5317 + - env: TOXENV=pypy3-xdist + python: 'pypy3' before_script: - | From 1f8fd421c4abf44f3de611772bd2bc90bbe80750 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Jun 2019 20:06:46 -0300 Subject: [PATCH 015/109] item.obj is again a bound method on TestCase function items Fix #5390 --- changelog/5390.bugfix.rst | 1 + src/_pytest/unittest.py | 2 ++ testing/test_unittest.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 changelog/5390.bugfix.rst diff --git a/changelog/5390.bugfix.rst b/changelog/5390.bugfix.rst new file mode 100644 index 00000000000..3f57c3043d5 --- /dev/null +++ b/changelog/5390.bugfix.rst @@ -0,0 +1 @@ +Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index da45e312f3a..216266979b7 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -108,11 +108,13 @@ class TestCaseFunction(Function): def setup(self): self._testcase = self.parent.obj(self.name) + self._obj = getattr(self._testcase, self.name) if hasattr(self, "_request"): self._request._fillfixtures() def teardown(self): self._testcase = None + self._obj = None def startTest(self, testcase): pass diff --git a/testing/test_unittest.py b/testing/test_unittest.py index a8555b35357..adcc232fd98 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -139,6 +139,29 @@ def test_func2(self): reprec.assertoutcome(passed=2) +def test_function_item_obj_is_instance(testdir): + """item.obj should be a bound method on unittest.TestCase function items (#5390).""" + testdir.makeconftest( + """ + def pytest_runtest_makereport(item, call): + if call.when == 'call': + class_ = item.parent.obj + assert isinstance(item.obj.__self__, class_) + """ + ) + testdir.makepyfile( + """ + import unittest + + class Test(unittest.TestCase): + def test_foo(self): + pass + """ + ) + result = testdir.runpytest_inprocess() + result.stdout.fnmatch_lines(["* 1 passed in*"]) + + def test_teardown(testdir): testpath = testdir.makepyfile( """ From 23cd68b667e3d55fd574df156915a5e26aa72415 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Jun 2019 21:10:59 -0300 Subject: [PATCH 016/109] Use keyword-only arguments in a few places --- src/_pytest/outcomes.py | 5 +---- src/_pytest/pytester.py | 36 ++++++++++-------------------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index c63c80e106d..0d2a318ed03 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -73,7 +73,7 @@ def exit(msg, returncode=None): exit.Exception = Exit -def skip(msg="", **kwargs): +def skip(msg="", *, allow_module_level=False): """ Skip an executing test with the given message. @@ -93,9 +93,6 @@ def skip(msg="", **kwargs): to skip a doctest statically. """ __tracebackhide__ = True - allow_module_level = kwargs.pop("allow_module_level", False) - if kwargs: - raise TypeError("unexpected keyword arguments: {}".format(sorted(kwargs))) raise Skipped(msg=msg, allow_module_level=allow_module_level) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index c7a8ca693e4..4f0342ab5d4 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -68,14 +68,6 @@ def pytest_configure(config): ) -def raise_on_kwargs(kwargs): - __tracebackhide__ = True - if kwargs: # pragma: no branch - raise TypeError( - "Unexpected keyword arguments: {}".format(", ".join(sorted(kwargs))) - ) - - class LsofFdLeakChecker: def get_open_files(self): out = self._exec_lsof() @@ -778,7 +770,7 @@ def inline_genitems(self, *args): items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec - def inline_run(self, *args, **kwargs): + def inline_run(self, *args, plugins=(), no_reraise_ctrlc=False): """Run ``pytest.main()`` in-process, returning a HookRecorder. Runs the :py:func:`pytest.main` function to run all of pytest inside @@ -789,15 +781,14 @@ def inline_run(self, *args, **kwargs): :param args: command line arguments to pass to :py:func:`pytest.main` - :param plugins: (keyword-only) extra plugin instances the - ``pytest.main()`` instance should use + :kwarg plugins: extra plugin instances the ``pytest.main()`` instance should use. + + :kwarg no_reraise_ctrlc: typically we reraise keyboard interrupts from the child run. If + True, the KeyboardInterrupt exception is captured. :return: a :py:class:`HookRecorder` instance """ - plugins = kwargs.pop("plugins", []) - no_reraise_ctrlc = kwargs.pop("no_reraise_ctrlc", None) - raise_on_kwargs(kwargs) - + plugins = list(plugins) finalizers = [] try: # Do not load user config (during runs only). @@ -1059,15 +1050,15 @@ def popen( return popen - def run(self, *cmdargs, **kwargs): + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN): """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. :param args: the sequence of arguments to pass to `subprocess.Popen()` - :param timeout: the period in seconds after which to timeout and raise + :kwarg timeout: the period in seconds after which to timeout and raise :py:class:`Testdir.TimeoutExpired` - :param stdin: optional standard input. Bytes are being send, closing + :kwarg stdin: optional standard input. Bytes are being send, closing the pipe, otherwise it is passed through to ``popen``. Defaults to ``CLOSE_STDIN``, which translates to using a pipe (``subprocess.PIPE``) that gets closed. @@ -1077,10 +1068,6 @@ def run(self, *cmdargs, **kwargs): """ __tracebackhide__ = True - timeout = kwargs.pop("timeout", None) - stdin = kwargs.pop("stdin", Testdir.CLOSE_STDIN) - raise_on_kwargs(kwargs) - cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1158,7 +1145,7 @@ def runpython_c(self, command): """Run python -c "command", return a :py:class:`RunResult`.""" return self.run(sys.executable, "-c", command) - def runpytest_subprocess(self, *args, **kwargs): + def runpytest_subprocess(self, *args, timeout=None): """Run pytest as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will be added using the @@ -1174,9 +1161,6 @@ def runpytest_subprocess(self, *args, **kwargs): Returns a :py:class:`RunResult`. """ __tracebackhide__ = True - timeout = kwargs.pop("timeout", None) - raise_on_kwargs(kwargs) - p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) From be2be040f95f7f47ea3fbb74ce12f5af4d483b97 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 4 Jun 2019 17:48:06 -0700 Subject: [PATCH 017/109] Clean up u' prefixes and py2 bytes conversions --- doc/en/tmpdir.rst | 2 +- src/_pytest/assertion/rewrite.py | 18 +++--------------- src/_pytest/assertion/util.py | 13 ++----------- src/_pytest/compat.py | 2 +- src/_pytest/pathlib.py | 4 +--- testing/acceptance_test.py | 14 +++----------- testing/code/test_source.py | 2 +- testing/python/collect.py | 2 +- testing/test_assertion.py | 2 +- testing/test_capture.py | 14 +++++++------- testing/test_doctest.py | 4 ++-- testing/test_junitxml.py | 6 +++--- testing/test_nose.py | 2 +- testing/test_pytester.py | 8 +++----- testing/test_runner.py | 8 +++----- testing/test_terminal.py | 1 - testing/test_warnings.py | 8 ++++---- 17 files changed, 37 insertions(+), 73 deletions(-) diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 330011bb348..01397a5ba2c 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -22,7 +22,7 @@ created in the `base temporary directory`_. # content of test_tmp_path.py import os - CONTENT = u"content" + CONTENT = "content" def test_create_file(tmp_path): diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ac9b85e6716..ce698f368f2 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -6,7 +6,6 @@ import marshal import os import re -import string import struct import sys import types @@ -336,8 +335,8 @@ def _write_pyc(state, co, source_stat, pyc): return True -RN = "\r\n".encode() -N = "\n".encode() +RN = b"\r\n" +N = b"\n" cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+") BOM_UTF8 = "\xef\xbb\xbf" @@ -420,15 +419,7 @@ def _saferepr(obj): JSON reprs. """ - r = saferepr(obj) - # only occurs in python2.x, repr must return text in python3+ - if isinstance(r, bytes): - # Represent unprintable bytes as `\x##` - r = "".join( - "\\x{:x}".format(ord(c)) if c not in string.printable else c.decode() - for c in r - ) - return r.replace("\n", "\\n") + return saferepr(obj).replace("\n", "\\n") def _format_assertmsg(obj): @@ -448,9 +439,6 @@ def _format_assertmsg(obj): obj = saferepr(obj) replaces.append(("\\n", "\n~")) - if isinstance(obj, bytes): - replaces = [(r1.encode(), r2.encode()) for r1, r2 in replaces] - for r1, r2 in replaces: obj = obj.replace(r1, r2) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 2be759bac21..9d6af5d69ab 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -13,15 +13,6 @@ _reprcompare = None -# the re-encoding is needed for python2 repr -# with non-ascii characters (see issue 877 and 1379) -def ecu(s): - if isinstance(s, bytes): - return s.decode("UTF-8", "replace") - else: - return s - - def format_explanation(explanation): """This formats an explanation @@ -32,7 +23,7 @@ def format_explanation(explanation): for when one explanation needs to span multiple lines, e.g. when displaying diffs. """ - explanation = ecu(explanation) + explanation = explanation lines = _split_explanation(explanation) result = _format_lines(lines) return "\n".join(result) @@ -135,7 +126,7 @@ def assertrepr_compare(config, op, left, right): left_repr = saferepr(left, maxsize=int(width // 2)) right_repr = saferepr(right, maxsize=width - len(left_repr)) - summary = "{} {} {}".format(ecu(left_repr), op, ecu(right_repr)) + summary = "{} {} {}".format(left_repr, op, right_repr) verbose = config.getoption("verbose") explanation = None diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index a4f3bc00319..6f1275e61cc 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -170,7 +170,7 @@ def ascii_escaped(val): """If val is pure ascii, returns it as a str(). Otherwise, escapes bytes objects into a sequence of escaped bytes: - b'\xc3\xb4\xc5\xd6' -> u'\\xc3\\xb4\\xc5\\xd6' + b'\xc3\xb4\xc5\xd6' -> '\\xc3\\xb4\\xc5\\xd6' and escapes unicode objects into a sequence of escaped unicode ids, e.g.: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 09b1bb3d53e..3269c25ed7b 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -134,9 +134,7 @@ def create_cleanup_lock(p): raise else: pid = os.getpid() - spid = str(pid) - if not isinstance(spid, bytes): - spid = spid.encode("ascii") + spid = str(pid).encode() os.write(fd, spid) os.close(fd) if not lock_path.is_file(): diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 9d903f80233..caa0d5191f2 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -510,7 +510,7 @@ def test_parametrized_with_null_bytes(self, testdir): """\ import pytest - @pytest.mark.parametrize("data", [b"\\x00", "\\x00", u'ação']) + @pytest.mark.parametrize("data", [b"\\x00", "\\x00", 'ação']) def test_foo(data): assert data """ @@ -998,16 +998,8 @@ def main(): def test_import_plugin_unicode_name(testdir): testdir.makepyfile(myplugin="") - testdir.makepyfile( - """ - def test(): pass - """ - ) - testdir.makeconftest( - """ - pytest_plugins = [u'myplugin'] - """ - ) + testdir.makepyfile("def test(): pass") + testdir.makeconftest("pytest_plugins = ['myplugin']") r = testdir.runpytest() assert r.ret == 0 diff --git a/testing/code/test_source.py b/testing/code/test_source.py index a12a102a057..15e0bf24ade 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -28,7 +28,7 @@ def test_source_str_function(): def test_unicode(): x = Source("4") assert str(x) == "4" - co = _pytest._code.compile('u"å"', mode="eval") + co = _pytest._code.compile('"å"', mode="eval") val = eval(co) assert isinstance(val, str) diff --git a/testing/python/collect.py b/testing/python/collect.py index 981e30fc3bc..d648452fd78 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -116,7 +116,7 @@ def test_show_traceback_import_error_unicode(self, testdir): """Check test modules collected which raise ImportError with unicode messages are handled properly (#2336). """ - testdir.makepyfile("raise ImportError(u'Something bad happened ☺')") + testdir.makepyfile("raise ImportError('Something bad happened ☺')") result = testdir.runpytest() result.stdout.fnmatch_lines( [ diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e651c09cef6..eced5461ad1 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1219,7 +1219,7 @@ def test_assert_with_unicode(monkeypatch, testdir): testdir.makepyfile( """\ def test_unicode(): - assert u'유니코드' == u'Unicode' + assert '유니코드' == 'Unicode' """ ) result = testdir.runpytest() diff --git a/testing/test_capture.py b/testing/test_capture.py index 0825745ad39..a85025c27d2 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -111,10 +111,10 @@ def test_unicode(): @pytest.mark.parametrize("method", ["fd", "sys"]) def test_capturing_bytes_in_utf8_encoding(testdir, method): testdir.makepyfile( - """ + """\ def test_unicode(): print('b\\u00f6y') - """ + """ ) result = testdir.runpytest("--capture=%s" % method) result.stdout.fnmatch_lines(["*1 passed*"]) @@ -511,7 +511,7 @@ def test_stdfd_functional(self, testdir): """\ def test_hello(capfd): import os - os.write(1, "42".encode('ascii')) + os.write(1, b"42") out, err = capfd.readouterr() assert out.startswith("42") capfd.close() @@ -564,7 +564,7 @@ def test_keyboardinterrupt_disables_capturing(self, testdir): """\ def test_hello(capfd): import os - os.write(1, str(42).encode('ascii')) + os.write(1, b'42') raise KeyboardInterrupt() """ ) @@ -1136,12 +1136,12 @@ class TestStdCaptureFD(TestStdCapture): def test_simple_only_fd(self, testdir): testdir.makepyfile( - """ + """\ import os def test_x(): - os.write(1, "hello\\n".encode("ascii")) + os.write(1, b"hello\\n") assert 0 - """ + """ ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines( diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 65c8cf3664f..bf0405546bf 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -153,7 +153,7 @@ def test_encoding(self, testdir, test_string, encoding): ) ) doctest = """ - >>> u"{}" + >>> "{}" {} """.format( test_string, repr(test_string) @@ -671,7 +671,7 @@ def test_print_unicode_value(self, testdir): test_print_unicode_value=r""" Here is a doctest:: - >>> print(u'\xE5\xE9\xEE\xF8\xFC') + >>> print('\xE5\xE9\xEE\xF8\xFC') åéîøü """ ) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 69b9c09c3f1..bcf83b352d8 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -873,12 +873,12 @@ def test_logxml_check_isdir(testdir): def test_escaped_parametrized_names_xml(testdir): testdir.makepyfile( - """ + """\ import pytest - @pytest.mark.parametrize('char', [u"\\x00"]) + @pytest.mark.parametrize('char', ["\\x00"]) def test_func(char): assert char - """ + """ ) result, dom = runandparse(testdir) assert result.ret == 0 diff --git a/testing/test_nose.py b/testing/test_nose.py index 8a3ce6454e7..f60c3af533a 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -370,7 +370,7 @@ def test_skip_test_with_unicode(testdir): import unittest class TestClass(): def test_io(self): - raise unittest.SkipTest(u'😊') + raise unittest.SkipTest('😊') """ ) result = testdir.runpytest() diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 82ff37c139a..96bf8504078 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -201,12 +201,10 @@ def test_makepyfile_utf8(testdir): """Ensure makepyfile accepts utf-8 bytes as input (#2738)""" utf8_contents = """ def setup_function(function): - mixed_encoding = u'São Paulo' - """.encode( - "utf-8" - ) + mixed_encoding = 'São Paulo' + """.encode() p = testdir.makepyfile(utf8_contents) - assert "mixed_encoding = u'São Paulo'".encode() in p.read("rb") + assert "mixed_encoding = 'São Paulo'".encode() in p.read("rb") class TestInlineRunModulesCleanup: diff --git a/testing/test_runner.py b/testing/test_runner.py index 77fdcecc3fa..13f722036dc 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -632,8 +632,7 @@ def some_internal_function(): assert "def some_internal_function()" not in result.stdout.str() -@pytest.mark.parametrize("str_prefix", ["u", ""]) -def test_pytest_fail_notrace_non_ascii(testdir, str_prefix): +def test_pytest_fail_notrace_non_ascii(testdir): """Fix pytest.fail with pytrace=False with non-ascii characters (#1178). This tests with native and unicode strings containing non-ascii chars. @@ -643,9 +642,8 @@ def test_pytest_fail_notrace_non_ascii(testdir, str_prefix): import pytest def test_hello(): - pytest.fail(%s'oh oh: ☺', pytrace=False) + pytest.fail('oh oh: ☺', pytrace=False) """ - % str_prefix ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*test_hello*", "oh oh: ☺"]) @@ -790,7 +788,7 @@ def pytest_runtest_makereport(): outcome = yield rep = outcome.get_result() if rep.when == "call": - rep.longrepr = u'ä' + rep.longrepr = 'ä' """ ) testdir.makepyfile( diff --git a/testing/test_terminal.py b/testing/test_terminal.py index f53cb6837bf..cc2c474ab45 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1672,7 +1672,6 @@ def check(msg, width, expected): check("😄😄😄😄😄\n2nd line", 29, "FAILED some::nodeid - 😄😄...") # NOTE: constructed, not sure if this is supported. - # It would fail if not using u"" in Python 2 for mocked_pos. mocked_pos = "nodeid::😄::withunicode" check("😄😄😄😄😄\n2nd line", 29, "FAILED nodeid::😄::withunicode") check("😄😄😄😄😄\n2nd line", 40, "FAILED nodeid::😄::withunicode - 😄😄...") diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 1654024785f..08a368521ba 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -130,7 +130,7 @@ def test_unicode(testdir, pyfile_with_warnings): @pytest.fixture def fix(): - warnings.warn(u"测试") + warnings.warn("测试") yield def test_func(fix): @@ -207,13 +207,13 @@ def test_show_warning(): def test_non_string_warning_argument(testdir): """Non-str argument passed to warning breaks pytest (#2956)""" testdir.makepyfile( - """ + """\ import warnings import pytest def test(): - warnings.warn(UserWarning(1, u'foo')) - """ + warnings.warn(UserWarning(1, 'foo')) + """ ) result = testdir.runpytest("-W", "always") result.stdout.fnmatch_lines(["*= 1 passed, 1 warnings in *"]) From aab568709324e3d90c0b493b18125c1f5d223dd6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 5 Jun 2019 11:21:44 +0200 Subject: [PATCH 018/109] tests: restore tracing function Without this, `testing/test_pdb.py` (already without pexpect) will cause missing test coverage afterwards (for the same process). --- testing/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/testing/conftest.py b/testing/conftest.py index 35d5f9661b4..635e7a6147e 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,5 +1,20 @@ +import sys + import pytest +if sys.gettrace(): + + @pytest.fixture(autouse=True) + def restore_tracing(): + """Restore tracing function (when run with Coverage.py). + + https://bugs.python.org/issue37011 + """ + orig_trace = sys.gettrace() + yield + if sys.gettrace() != orig_trace: + sys.settrace(orig_trace) + @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems(config, items): From d9eafbdee3401538cf9dc594df5006d1d3b72f8a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 5 Jun 2019 01:58:45 +0200 Subject: [PATCH 019/109] Revert "Enable coverage for 'py37' environment" This reverts commit 6d393c5dc8bfc9e437f85dae83914004409b1bc2. It should not be necessary, because we have it via other jobs already. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 61d4df4dfec..0b62b133c24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ jobs: - test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 37 # Full run of latest supported version, without xdist. - - env: TOXENV=py37 PYTEST_COVERAGE=1 + - env: TOXENV=py37 python: '3.7' # Coverage tracking is slow with pypy, skip it. From 8f5cb461a8730501235eae611dda6b5b6edc539c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Jun 2019 19:02:52 -0300 Subject: [PATCH 020/109] Turn PytestDeprecationWarning into error Fix #5402 --- changelog/5402.removal.rst | 23 +++++++++++++++++++++++ src/_pytest/warnings.py | 1 + testing/python/fixtures.py | 3 +-- testing/test_warnings.py | 31 +++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 changelog/5402.removal.rst diff --git a/changelog/5402.removal.rst b/changelog/5402.removal.rst new file mode 100644 index 00000000000..29921dd9763 --- /dev/null +++ b/changelog/5402.removal.rst @@ -0,0 +1,23 @@ +**PytestDeprecationWarning are now errors by default.** + +Following our plan to remove deprecated features with as little disruption as +possible, all warnings of type ``PytestDeprecationWarning`` now generate errors +instead of warning messages. + +**The affected features will be effectively removed in pytest 5.1**, so please consult the +`Deprecations and Removals `__ +section in the docs for directions on how to update existing code. + +In the pytest ``5.0.X`` series, it is possible to change the errors back into warnings as a stop +gap measure by adding this to your ``pytest.ini`` file: + +.. code-block:: ini + + [pytest] + filterwarnings = + ignore::pytest.PytestDeprecationWarning + +But this will stop working when pytest ``5.1`` is released. + +**If you have concerns** about the removal of a specific feature, please add a +comment to `#5402 `__. diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 2f10862251a..f47eee0d4f0 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -75,6 +75,7 @@ def catch_warnings_for_item(config, ihook, when, item): warnings.filterwarnings("always", category=PendingDeprecationWarning) warnings.filterwarnings("error", category=pytest.RemovedInPytest4Warning) + warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning) # filters should have this precedence: mark, cmdline options, ini # filters should be applied in the inverse order of precedence diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a9ea333adc4..75467fb09f1 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1138,7 +1138,6 @@ def __init__(self, request): values = reprec.getfailedcollections() assert len(values) == 1 - @pytest.mark.filterwarnings("ignore::pytest.PytestDeprecationWarning") def test_request_can_be_overridden(self, testdir): testdir.makepyfile( """ @@ -1151,7 +1150,7 @@ def test_request(request): assert request.a == 1 """ ) - reprec = testdir.inline_run() + reprec = testdir.inline_run("-Wignore::pytest.PytestDeprecationWarning") reprec.assertoutcome(passed=1) def test_usefixtures_marker(self, testdir): diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 08a368521ba..2ce83ae8839 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -528,6 +528,37 @@ def test(): result.stdout.fnmatch_lines(["* 1 passed in *"]) +@pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) +def test_deprecation_warning_as_error(testdir, change_default): + testdir.makepyfile( + """ + import warnings, pytest + def test(): + warnings.warn(pytest.PytestDeprecationWarning("some warning")) + """ + ) + if change_default == "ini": + testdir.makeini( + """ + [pytest] + filterwarnings = + ignore::pytest.PytestDeprecationWarning + """ + ) + + args = ( + ("-Wignore::pytest.PytestDeprecationWarning",) + if change_default == "cmdline" + else () + ) + result = testdir.runpytest(*args) + if change_default is None: + result.stdout.fnmatch_lines(["* 1 failed in *"]) + else: + assert change_default in ("ini", "cmdline") + result.stdout.fnmatch_lines(["* 1 passed in *"]) + + class TestAssertionWarnings: @staticmethod def assert_result_warns(result, msg): From 577b0dffe75eda21d2ae8db8480112341153a2d6 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 4 Jun 2019 23:43:40 +0200 Subject: [PATCH 021/109] Fix verbosity bug in --collect-only --- changelog/5383.bugfix.rst | 2 ++ src/_pytest/logging.py | 13 +++++++--- testing/logging/test_reporting.py | 43 ++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 changelog/5383.bugfix.rst diff --git a/changelog/5383.bugfix.rst b/changelog/5383.bugfix.rst new file mode 100644 index 00000000000..53e25956d56 --- /dev/null +++ b/changelog/5383.bugfix.rst @@ -0,0 +1,2 @@ +``-q`` has again an impact on the style of the collected items +(``--collect-only``) when ``--log-cli-level`` is used. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index ac0c4c2b337..df18b81cdc0 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -409,10 +409,6 @@ def __init__(self, config): """ self._config = config - # enable verbose output automatically if live logging is enabled - if self._log_cli_enabled() and config.getoption("verbose") < 1: - config.option.verbose = 1 - self.print_logs = get_option_ini(config, "log_print") self.formatter = self._create_formatter( get_option_ini(config, "log_format"), @@ -628,6 +624,15 @@ def pytest_sessionstart(self): @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session): """Runs all collected test items.""" + + if session.config.option.collectonly: + yield + return + + if self._log_cli_enabled() and self._config.getoption("verbose") < 1: + # setting verbose flag is needed to avoid messy test progress output + self._config.option.verbose = 1 + with self.live_logs_context(): if self.log_file_handler is not None: with catching_logs(self.log_file_handler, level=self.log_file_level): diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 68be819b9a4..bb1aebc097a 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -916,14 +916,45 @@ def test_collection_live_logging(testdir): result = testdir.runpytest("--log-cli-level=INFO") result.stdout.fnmatch_lines( - [ - "collecting*", - "*--- live log collection ---*", - "*Normal message*", - "collected 0 items", - ] + ["*--- live log collection ---*", "*Normal message*", "collected 0 items"] + ) + + +@pytest.mark.parametrize("verbose", ["", "-q", "-qq"]) +def test_collection_collect_only_live_logging(testdir, verbose): + testdir.makepyfile( + """ + def test_simple(): + pass + """ ) + result = testdir.runpytest("--collect-only", "--log-cli-level=INFO", verbose) + + expected_lines = [] + + if not verbose: + expected_lines.extend( + [ + "*collected 1 item*", + "**", + "*no tests ran*", + ] + ) + elif verbose == "-q": + assert "collected 1 item*" not in result.stdout.str() + expected_lines.extend( + [ + "*test_collection_collect_only_live_logging.py::test_simple*", + "no tests ran in * seconds", + ] + ) + elif verbose == "-qq": + assert "collected 1 item*" not in result.stdout.str() + expected_lines.extend(["*test_collection_collect_only_live_logging.py: 1*"]) + + result.stdout.fnmatch_lines(expected_lines) + def test_collection_logging_to_file(testdir): log_file = testdir.tmpdir.join("pytest.log").strpath From 0fd1f3038ced3df291c0ce3fca6f34e69bf75f10 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 5 Jun 2019 03:47:36 +0200 Subject: [PATCH 022/109] ci: move coverage reporting to shared script --- .travis.yml | 8 +------- azure-pipelines.yml | 22 ++++++++++++++-------- scripts/report-coverage.sh | 16 ++++++++++++++++ scripts/setup-coverage-vars.bat | 7 ------- scripts/upload-coverage.bat | 16 ---------------- 5 files changed, 31 insertions(+), 38 deletions(-) create mode 100755 scripts/report-coverage.sh delete mode 100644 scripts/setup-coverage-vars.bat delete mode 100644 scripts/upload-coverage.bat diff --git a/.travis.yml b/.travis.yml index 0b62b133c24..073017a996f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,13 +114,7 @@ script: tox after_success: - | if [[ "$PYTEST_COVERAGE" = 1 ]]; then - set -e - # Add last TOXENV to $PATH. - PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH" - coverage combine - coverage xml - coverage report -m - bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml -n $TOXENV-$TRAVIS_OS_NAME + env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh fi notifications: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 72e4af732fe..b3515d5e0a9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,8 +4,6 @@ trigger: variables: PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml -vv" - COVERAGE_FILE: "$(Build.Repository.LocalPath)/.coverage" - COVERAGE_PROCESS_START: "$(Build.Repository.LocalPath)/.coveragerc" PYTEST_COVERAGE: '0' jobs: @@ -55,8 +53,13 @@ jobs: - script: python -m pip install --upgrade pip && python -m pip install tox displayName: 'Install tox' - - script: | - call scripts/setup-coverage-vars.bat || goto :eof + - bash: | + if [[ "$PYTEST_COVERAGE" == "1" ]]; then + export _PYTEST_TOX_COVERAGE_RUN="coverage run -m" + export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess + export COVERAGE_FILE="$PWD/.coverage" + export COVERAGE_PROCESS_START="$PWD/.coveragerc" + fi python -m tox -e $(tox.env) displayName: 'Run tests' @@ -66,9 +69,12 @@ jobs: testRunTitle: '$(tox.env)' condition: succeededOrFailed() - - script: call scripts\upload-coverage.bat - displayName: 'Report and upload coverage' - condition: eq(variables['PYTEST_COVERAGE'], '1') + - bash: | + if [[ "$PYTEST_COVERAGE" == 1 ]]; then + scripts/report-coverage.sh + fi env: + CODECOV_NAME: $(tox.env) CODECOV_TOKEN: $(CODECOV_TOKEN) - PYTEST_CODECOV_NAME: $(tox.env) + displayName: Report and upload coverage + condition: eq(variables['PYTEST_COVERAGE'], '1') diff --git a/scripts/report-coverage.sh b/scripts/report-coverage.sh new file mode 100755 index 00000000000..755783d2adf --- /dev/null +++ b/scripts/report-coverage.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [ -z "$TOXENV" ]; then + python -m pip install coverage +else + # Add last TOXENV to $PATH. + PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH" +fi + +python -m coverage combine +python -m coverage xml +python -m coverage report -m +bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml diff --git a/scripts/setup-coverage-vars.bat b/scripts/setup-coverage-vars.bat deleted file mode 100644 index 7a4a6d4deb8..00000000000 --- a/scripts/setup-coverage-vars.bat +++ /dev/null @@ -1,7 +0,0 @@ -if "%PYTEST_COVERAGE%" == "1" ( - set "_PYTEST_TOX_COVERAGE_RUN=coverage run -m" - set "_PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess" - echo Coverage vars configured, PYTEST_COVERAGE=%PYTEST_COVERAGE% -) else ( - echo Skipping coverage vars setup, PYTEST_COVERAGE=%PYTEST_COVERAGE% -) diff --git a/scripts/upload-coverage.bat b/scripts/upload-coverage.bat deleted file mode 100644 index 08ed5779122..00000000000 --- a/scripts/upload-coverage.bat +++ /dev/null @@ -1,16 +0,0 @@ -REM script called by Azure to combine and upload coverage information to codecov -if "%PYTEST_COVERAGE%" == "1" ( - echo Prepare to upload coverage information - if defined CODECOV_TOKEN ( - echo CODECOV_TOKEN defined - ) else ( - echo CODECOV_TOKEN NOT defined - ) - python -m pip install codecov - python -m coverage combine - python -m coverage xml - python -m coverage report -m - scripts\retry python -m codecov --required -X gcov pycov search -f coverage.xml --name %PYTEST_CODECOV_NAME% -) else ( - echo Skipping coverage upload, PYTEST_COVERAGE=%PYTEST_COVERAGE% -) From 65c2a81924a0e82f4926322deea02d8c4709c37c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 6 Jun 2019 12:20:32 -0300 Subject: [PATCH 023/109] Remove ExceptionInfo.__str__, falling back to __repr__ Fix #5412 --- changelog/5412.removal.rst | 2 ++ src/_pytest/_code/code.py | 7 ------- testing/code/test_excinfo.py | 14 +++----------- 3 files changed, 5 insertions(+), 18 deletions(-) create mode 100644 changelog/5412.removal.rst diff --git a/changelog/5412.removal.rst b/changelog/5412.removal.rst new file mode 100644 index 00000000000..a6f19700629 --- /dev/null +++ b/changelog/5412.removal.rst @@ -0,0 +1,2 @@ +``ExceptionInfo`` objects (returned by ``pytest.raises``) now have the same ``str`` representation as ``repr``, which +avoids some confusion when users use ``print(e)`` to inspect the object. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index c4ed961ace4..9644c61ec50 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -534,13 +534,6 @@ def getrepr( ) return fmt.repr_excinfo(self) - def __str__(self): - if self._excinfo is None: - return repr(self) - entry = self.traceback[-1] - loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return str(loc) - def match(self, regexp): """ Check whether the regular expression 'regexp' is found in the string diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 3eac94a287e..f7787c282f7 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -333,18 +333,10 @@ def test_excinfo_exconly(): assert msg.endswith("world") -def test_excinfo_repr(): +def test_excinfo_repr_str(): excinfo = pytest.raises(ValueError, h) - s = repr(excinfo) - assert s == "" - - -def test_excinfo_str(): - excinfo = pytest.raises(ValueError, h) - s = str(excinfo) - assert s.startswith(__file__[:-9]) # pyc file and $py.class - assert s.endswith("ValueError") - assert len(s.split(":")) >= 3 # on windows it's 4 + assert repr(excinfo) == "" + assert str(excinfo) == "" def test_excinfo_for_later(): From ccd87f9e800e135fadbc56cdb9060f83448e760e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 6 Jun 2019 09:13:02 -0700 Subject: [PATCH 024/109] small mypy fixes --- src/_pytest/assertion/util.py | 11 ++--------- src/_pytest/outcomes.py | 2 +- testing/io/test_saferepr.py | 2 +- testing/python/fixtures.py | 4 ++-- testing/python/raises.py | 2 +- testing/test_pytester.py | 7 +------ testing/test_tmpdir.py | 1 - 7 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 9d6af5d69ab..f2cb2ab6374 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -81,19 +81,12 @@ def _format_lines(lines): return result -# Provide basestring in python3 -try: - basestring = basestring -except NameError: - basestring = str - - def issequence(x): - return isinstance(x, Sequence) and not isinstance(x, basestring) + return isinstance(x, Sequence) and not isinstance(x, str) def istext(x): - return isinstance(x, basestring) + return isinstance(x, str) def isdict(x): diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 0d2a318ed03..749e80f3cb6 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -114,7 +114,7 @@ def fail(msg="", pytrace=True): fail.Exception = Failed -class XFailed(fail.Exception): +class XFailed(Failed): """ raised from an explicit call to pytest.xfail() """ diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index e57b53f81e9..f6abfe3228c 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -15,7 +15,7 @@ def test_maxsize(): def test_maxsize_error_on_instance(): class A: - def __repr__(): + def __repr__(self): raise ValueError("...") s = saferepr(("*" * 50, A()), maxsize=25) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 75467fb09f1..1d39079ea92 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -26,10 +26,10 @@ def h(arg1, arg2="hello"): assert fixtures.getfuncargnames(h) == ("arg1",) - def h(arg1, arg2, arg3="hello"): + def j(arg1, arg2, arg3="hello"): pass - assert fixtures.getfuncargnames(h) == ("arg1", "arg2") + assert fixtures.getfuncargnames(j) == ("arg1", "arg2") class A: def f(self, arg1, arg2="hello"): diff --git a/testing/python/raises.py b/testing/python/raises.py index bfcb3dbb163..89cef38f1a2 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -235,7 +235,7 @@ def test_raises_match_wrong_type(self): int("asdf") def test_raises_exception_looks_iterable(self): - class Meta(type(object)): + class Meta(type): def __getitem__(self, item): return 1 / 0 diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 96bf8504078..ca12672f598 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -189,12 +189,7 @@ def test_hookrecorder_basic(holder): def test_makepyfile_unicode(testdir): - global unichr - try: - unichr(65) - except NameError: - unichr = chr - testdir.makepyfile(unichr(0xFFFD)) + testdir.makepyfile(chr(0xFFFD)) def test_makepyfile_utf8(testdir): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index a7c0ed7eaa7..c4c7ebe256e 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -24,7 +24,6 @@ def test_ensuretemp(recwarn): @attr.s class FakeConfig: basetemp = attr.ib() - trace = attr.ib(default=None) @property def trace(self): From 918268774ba99f60dd809bee50946a4438da605e Mon Sep 17 00:00:00 2001 From: Ralph Giles Date: Thu, 6 Jun 2019 09:41:15 -0700 Subject: [PATCH 025/109] Add `slow` marker in run/skip option example. The example implementation of a `--runslow` option results in a `PytestUnknownMarkWarning`. Include registering the custom mark in the example, based on the documentation in markers.rst. --- AUTHORS | 1 + changelog/5416.doc.rst | 1 + doc/en/example/simple.rst | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 changelog/5416.doc.rst diff --git a/AUTHORS b/AUTHORS index 0672d4abf6e..3d050a346dc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -200,6 +200,7 @@ Pulkit Goyal Punyashloka Biswal Quentin Pradet Ralf Schmitt +Ralph Giles Ran Benita Raphael Castaneda Raphael Pierzina diff --git a/changelog/5416.doc.rst b/changelog/5416.doc.rst new file mode 100644 index 00000000000..81e4c640441 --- /dev/null +++ b/changelog/5416.doc.rst @@ -0,0 +1 @@ +Fix PytestUnknownMarkWarning in run/skip example. diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 140f4b840f1..5e405da569b 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -157,6 +157,10 @@ line option to control skipping of ``pytest.mark.slow`` marked tests: ) + def pytest_configure(config): + config.addinivalue_line("markers", "slow: mark test as slow to run") + + def pytest_collection_modifyitems(config, items): if config.getoption("--runslow"): # --runslow given in cli: do not skip slow tests From 47022b36cb5e235d26c1d50503c0705faf56e378 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 7 Jun 2019 11:01:23 +0200 Subject: [PATCH 026/109] ci: Travis: remove pypy3 job for now Ref: https://github.com/pytest-dev/pytest/issues/5317#issuecomment-499019928 --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 073017a996f..0fa2ff0dee0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,10 +37,6 @@ jobs: - env: TOXENV=py37 python: '3.7' - # Coverage tracking is slow with pypy, skip it. - - env: TOXENV=pypy3-xdist - python: 'pypy3' - - env: TOXENV=py35-xdist python: '3.5' @@ -90,10 +86,6 @@ matrix: allow_failures: - python: '3.8-dev' env: TOXENV=py38-xdist - # failing due to ResourceWarning exceptions: - # https://github.com/pytest-dev/pytest/issues/5317 - - env: TOXENV=pypy3-xdist - python: 'pypy3' before_script: - | From f0cee593f2ce4bd53c94f76d588697f08194b67d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 7 Jun 2019 10:59:40 -0300 Subject: [PATCH 027/109] Link deprecation docs pytest.raises 'message' warning As commented in https://github.com/pytest-dev/pytest/issues/3974#issuecomment-499870914 --- src/_pytest/deprecated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index f80773fe577..b6b3bddf71a 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -35,8 +35,8 @@ RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning( "The 'message' parameter is deprecated.\n" "(did you mean to use `match='some regex'` to check the exception message?)\n" - "Please comment on https://github.com/pytest-dev/pytest/issues/3974 " - "if you have concerns about removal of this parameter." + "Please see:\n" + " https://docs.pytest.org/en/4.6-maintenance/deprecations.html#message-parameter-of-pytest-raises" ) RESULT_LOG = PytestDeprecationWarning( From 28aa38ece65a8cff6be95911854ecfabab9eac7a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 5 Jun 2019 07:24:50 +0200 Subject: [PATCH 028/109] ci: optimize twisted/pexpect related jobs - tox: use twisted as dep only - Azure: move twisted/numpy to main py37 job - Travis: move twisted to main py37 build --- .travis.yml | 7 ++----- azure-pipelines.yml | 5 +---- tox.ini | 5 +---- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0fa2ff0dee0..81207efbed2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,9 +20,6 @@ jobs: include: # OSX tests - first (in test stage), since they are the slower ones. - os: osx - # NOTE: (tests with) pexpect appear to be buggy on Travis, - # at least with coverage. - # Log: https://travis-ci.org/pytest-dev/pytest/jobs/500358864 osx_image: xcode10.1 language: generic env: TOXENV=py37-xdist PYTEST_COVERAGE=1 @@ -45,12 +42,12 @@ jobs: # - TestArgComplete (linux only) # - numpy # Empty PYTEST_ADDOPTS to run this non-verbose. - - env: TOXENV=py37-lsof-numpy-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS= + - env: TOXENV=py37-lsof-numpy-twisted-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS= # Specialized factors for py37. # Coverage for: # - test_sys_breakpoint_interception (via pexpect). - - env: TOXENV=py37-pexpect,py37-twisted PYTEST_COVERAGE=1 + - env: TOXENV=py37-pexpect PYTEST_COVERAGE=1 - env: TOXENV=py37-pluggymaster-xdist - env: TOXENV=py37-freeze diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b3515d5e0a9..f18ce08877a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,7 +28,7 @@ jobs: tox.env: 'py36-xdist' py37: python.version: '3.7' - tox.env: 'py37' + tox.env: 'py37-twisted-numpy' # Coverage for: # - _py36_windowsconsoleio_workaround (with py36+) # - test_request_garbage (no xdist) @@ -36,9 +36,6 @@ jobs: py37-linting/docs/doctesting: python.version: '3.7' tox.env: 'linting,docs,doctesting' - py37-twisted/numpy: - python.version: '3.7' - tox.env: 'py37-twisted,py37-numpy' py37-pluggymaster-xdist: python.version: '3.7' tox.env: 'py37-pluggymaster-xdist' diff --git a/tox.ini b/tox.ini index 7e5b182f69b..21cdd4d3fc0 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ commands = coverage: coverage report passenv = USER USERNAME COVERAGE_* TRAVIS PYTEST_ADDOPTS setenv = - _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_PEXPECT:} {env:_PYTEST_TOX_POSARGS_TWISTED:} {env:_PYTEST_TOX_POSARGS_XDIST:} + _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_XDIST:} # Configuration to run with coverage similar to Travis/Appveyor, e.g. # "tox -e py37-coverage". @@ -37,9 +37,6 @@ setenv = lsof: _PYTEST_TOX_POSARGS_LSOF=--lsof pexpect: _PYTEST_TOX_PLATFORM=linux|darwin - pexpect: _PYTEST_TOX_POSARGS_PEXPECT=-m uses_pexpect - - twisted: _PYTEST_TOX_POSARGS_TWISTED=testing/test_unittest.py xdist: _PYTEST_TOX_POSARGS_XDIST=-n auto extras = testing From e868bb647d29781d0c56fab19482f7f98f276d47 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 1 Jun 2019 23:53:03 +0200 Subject: [PATCH 029/109] remove commented code --- src/_pytest/_code/code.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 9644c61ec50..b0b4d653119 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -910,7 +910,6 @@ def toterminal(self, tw): for line in self.lines: red = line.startswith("E ") tw.line(line, bold=True, red=red) - # tw.line("") return if self.reprfuncargs: self.reprfuncargs.toterminal(tw) From 75cda6de53ebb704f3026a32b448e864eee3dba9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 9 Jun 2019 11:54:29 +0200 Subject: [PATCH 030/109] tox: coverage: use -m with coverage-report --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7e5b182f69b..1f8d10451b2 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ envlist = commands = {env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:{env:_PYTEST_TOX_DEFAULT_POSARGS:}} coverage: coverage combine - coverage: coverage report + coverage: coverage report -m passenv = USER USERNAME COVERAGE_* TRAVIS PYTEST_ADDOPTS setenv = _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_PEXPECT:} {env:_PYTEST_TOX_POSARGS_TWISTED:} {env:_PYTEST_TOX_POSARGS_XDIST:} From c5a549b5bb15452aecb66cead19c586336fa4c21 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Sat, 8 Jun 2019 12:39:59 +1000 Subject: [PATCH 031/109] Emit warning for broken object --- changelog/5404.bugfix.rst | 2 ++ src/_pytest/doctest.py | 16 +++++++++++++--- testing/test_doctest.py | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 changelog/5404.bugfix.rst diff --git a/changelog/5404.bugfix.rst b/changelog/5404.bugfix.rst new file mode 100644 index 00000000000..2187bed8b32 --- /dev/null +++ b/changelog/5404.bugfix.rst @@ -0,0 +1,2 @@ +Emit a warning when attempting to unwrap a broken object raises an exception, +for easier debugging (`#5080 `__). diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index afb7ede4cc7..50c81902684 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -3,6 +3,7 @@ import platform import sys import traceback +import warnings from contextlib import contextmanager import pytest @@ -12,6 +13,7 @@ from _pytest.compat import safe_getattr from _pytest.fixtures import FixtureRequest from _pytest.outcomes import Skipped +from _pytest.warning_types import PytestWarning DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" @@ -368,10 +370,18 @@ def _patch_unwrap_mock_aware(): else: def _mock_aware_unwrap(obj, stop=None): - if stop is None: - return real_unwrap(obj, stop=_is_mocked) - else: + try: + if stop is None or stop is _is_mocked: + return real_unwrap(obj, stop=_is_mocked) return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj)) + except Exception as e: + warnings.warn( + "Got %r when unwrapping %r. This is usually caused " + "by a violation of Python's object protocol; see e.g. " + "https://github.com/pytest-dev/pytest/issues/5080" % (e, obj), + PytestWarning, + ) + raise inspect.unwrap = _mock_aware_unwrap try: diff --git a/testing/test_doctest.py b/testing/test_doctest.py index bf0405546bf..23606667308 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,7 +1,10 @@ +import inspect import textwrap import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR +from _pytest.doctest import _is_mocked +from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule from _pytest.doctest import DoctestTextfile @@ -1224,3 +1227,24 @@ class Example(object): ) result = testdir.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) + + +class Broken: + def __getattr__(self, _): + raise KeyError("This should be an AttributeError") + + +@pytest.mark.parametrize( # pragma: no branch (lambdas are not called) + "stop", [None, _is_mocked, lambda f: None, lambda f: False, lambda f: True] +) +def test_warning_on_unwrap_of_broken_object(stop): + bad_instance = Broken() + assert inspect.unwrap.__module__ == "inspect" + with _patch_unwrap_mock_aware(): + assert inspect.unwrap.__module__ != "inspect" + with pytest.warns( + pytest.PytestWarning, match="^Got KeyError.* when unwrapping" + ): + with pytest.raises(KeyError): + inspect.unwrap(bad_instance, stop=stop) + assert inspect.unwrap.__module__ == "inspect" From 18c2ff662582a52ee3d59c1feab93397426286c1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 11 Jun 2019 09:59:05 -0700 Subject: [PATCH 032/109] Merge pull request #5435 from asottile/release-4.6.3 Preparing release version 4.6.3 --- CHANGELOG.rst | 16 ++++++++++++++++ doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.3.rst | 21 +++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 doc/en/announce/release-4.6.3.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 715238b327b..45e31bef94b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,22 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.3 (2019-06-11) +========================= + +Bug Fixes +--------- + +- `#5383 `_: ``-q`` has again an impact on the style of the collected items + (``--collect-only``) when ``--log-cli-level`` is used. + + +- `#5389 `_: Fix regressions of `#5063 `__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. + + +- `#5390 `_: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. + + pytest 4.6.2 (2019-06-03) ========================= diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 9379ae5b1f8..c8c7f243a4b 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.3 release-4.6.2 release-4.6.1 release-4.6.0 diff --git a/doc/en/announce/release-4.6.3.rst b/doc/en/announce/release-4.6.3.rst new file mode 100644 index 00000000000..0bfb355a15a --- /dev/null +++ b/doc/en/announce/release-4.6.3.rst @@ -0,0 +1,21 @@ +pytest-4.6.3 +======================================= + +pytest 4.6.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Dirk Thomas + + +Happy testing, +The pytest Development Team From 108fad1ac0c494686d6131e766721aedf3de5b04 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 11 Jun 2019 10:53:32 -0700 Subject: [PATCH 033/109] Revert "ci: Travis: remove pypy3 job for now" This reverts commit 47022b36cb5e235d26c1d50503c0705faf56e378. --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 81207efbed2..78ae405fbb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,10 @@ jobs: - env: TOXENV=py37 python: '3.7' + # Coverage tracking is slow with pypy, skip it. + - env: TOXENV=pypy3-xdist + python: 'pypy3' + - env: TOXENV=py35-xdist python: '3.5' @@ -83,6 +87,10 @@ matrix: allow_failures: - python: '3.8-dev' env: TOXENV=py38-xdist + # failing due to ResourceWarning exceptions: + # https://github.com/pytest-dev/pytest/issues/5317 + - env: TOXENV=pypy3-xdist + python: 'pypy3' before_script: - | From f586d627b343660d75e7bcb77157c2058bdc0f81 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 11 Jun 2019 10:54:16 -0700 Subject: [PATCH 034/109] re-enable pypy3 now that importlib-metadata 0.18 is released --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 78ae405fbb7..2dd424e58de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,10 +87,6 @@ matrix: allow_failures: - python: '3.8-dev' env: TOXENV=py38-xdist - # failing due to ResourceWarning exceptions: - # https://github.com/pytest-dev/pytest/issues/5317 - - env: TOXENV=pypy3-xdist - python: 'pypy3' before_script: - | From 52780f39ceeed7c5349d6a8998080ab105dd86bb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 11 Jun 2019 21:19:30 -0300 Subject: [PATCH 035/109] Postpone removal of --result-log to pytest 6.0 As we did not provide an alternative yet, it is better to postpone the actual removal until we have provided a suitable and stable alternative. Related to #4488 --- changelog/4488.deprecation.rst | 2 ++ doc/en/deprecations.rst | 17 +++++++++-------- src/_pytest/deprecated.py | 2 +- testing/deprecated_test.py | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 changelog/4488.deprecation.rst diff --git a/changelog/4488.deprecation.rst b/changelog/4488.deprecation.rst new file mode 100644 index 00000000000..575df554539 --- /dev/null +++ b/changelog/4488.deprecation.rst @@ -0,0 +1,2 @@ +The removal of the ``--result-log`` option and module has been postponed to (tentatively) pytest 6.0 as +the team has not yet got around to implement a good alternative for it. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index a505c0a94ac..6b4f360b581 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -101,20 +101,21 @@ Becomes: - - - Result log (``--result-log``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. deprecated:: 4.0 +The ``--result-log`` option produces a stream of test reports which can be +analysed at runtime. It uses a custom format which requires users to implement their own +parser, but the team believes using a line-based format that can be parsed using standard +tools would provide a suitable and better alternative. -The ``--resultlog`` command line option has been deprecated: it is little used -and there are more modern and better alternatives, for example `pytest-tap `_. - -This feature will be effectively removed in pytest 4.0 as the team intends to include a better alternative in the core. +The current plan is to provide an alternative in the pytest 5.0 series and remove the ``--result-log`` +option in pytest 6.0 after the new implementation proves satisfactory to all users and is deemed +stable. -If you have any concerns, please don't hesitate to `open an issue `__. +The actual alternative is still being discussed in issue `#4488 `__. Removed Features ---------------- diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index b6b3bddf71a..3feae8b4346 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -40,7 +40,7 @@ ) RESULT_LOG = PytestDeprecationWarning( - "--result-log is deprecated and scheduled for removal in pytest 5.0.\n" + "--result-log is deprecated and scheduled for removal in pytest 6.0.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index f64db798b2c..177594c4a55 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -49,7 +49,7 @@ def test(): result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log")) result.stdout.fnmatch_lines( [ - "*--result-log is deprecated and scheduled for removal in pytest 5.0*", + "*--result-log is deprecated and scheduled for removal in pytest 6.0*", "*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*", ] ) From 701d0351a6df84b9b9fc4b80562cd5de65db71c4 Mon Sep 17 00:00:00 2001 From: patriksevallius Date: Thu, 13 Jun 2019 06:01:30 +0200 Subject: [PATCH 036/109] Add missing 'e' to test_mod(e). --- doc/en/nose.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/nose.rst b/doc/en/nose.rst index 701ea44eca1..bf416132c5c 100644 --- a/doc/en/nose.rst +++ b/doc/en/nose.rst @@ -46,7 +46,7 @@ Unsupported idioms / known issues `_. - nose imports test modules with the same import path (e.g. - ``tests.test_mod``) but different file system paths + ``tests.test_mode``) but different file system paths (e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``) by extending sys.path/import semantics. pytest does not do that but there is discussion in `#268 `_ for adding some support. Note that From ad15efc7ea3e2790b296853e69ea705491410e0e Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 13 Jun 2019 12:38:33 +0100 Subject: [PATCH 037/109] add test for stepwise attribute error Refs: #5444 --- testing/test_stepwise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 2202bbf1b60..d508f4a86c9 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -161,8 +161,8 @@ def test_stop_on_collection_errors(broken_testdir): "-v", "--strict-markers", "--stepwise", - "working_testfile.py", "broken_testfile.py", + "working_testfile.py", ) stdout = result.stdout.str() From 4cc05a657d4ed4b78a723f250a3bfa8eaa7cc0bd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Jun 2019 16:45:27 -0300 Subject: [PATCH 038/109] Fix --sw crash when first file in cmdline fails to collect Fix #5444 --- changelog/5444.bugfix.rst | 1 + src/_pytest/stepwise.py | 3 ++- testing/test_stepwise.py | 26 +++++++++++++++----------- 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 changelog/5444.bugfix.rst diff --git a/changelog/5444.bugfix.rst b/changelog/5444.bugfix.rst new file mode 100644 index 00000000000..230d4b49eb6 --- /dev/null +++ b/changelog/5444.bugfix.rst @@ -0,0 +1 @@ +Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect. diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 68e53a31cb4..4a7e4d9b3fe 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -28,6 +28,7 @@ def __init__(self, config): self.config = config self.active = config.getvalue("stepwise") self.session = None + self.report_status = "" if self.active: self.lastfailed = config.cache.get("cache/stepwise", None) @@ -103,7 +104,7 @@ def pytest_runtest_logreport(self, report): self.lastfailed = None def pytest_report_collectionfinish(self): - if self.active and self.config.getoption("verbose") >= 0: + if self.active and self.config.getoption("verbose") >= 0 and self.report_status: return "stepwise: %s" % self.report_status def pytest_sessionfinish(self, session): diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index d508f4a86c9..4b018c72222 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -156,14 +156,18 @@ def test_change_testfile(stepwise_testdir): assert "test_success PASSED" in stdout -def test_stop_on_collection_errors(broken_testdir): - result = broken_testdir.runpytest( - "-v", - "--strict-markers", - "--stepwise", - "broken_testfile.py", - "working_testfile.py", - ) - - stdout = result.stdout.str() - assert "errors during collection" in stdout +@pytest.mark.parametrize("broken_first", [True, False]) +def test_stop_on_collection_errors(broken_testdir, broken_first): + """Stop during collection errors. We have two possible messages depending on the order (#5444), + so test both cases.""" + files = ["working_testfile.py", "broken_testfile.py"] + if broken_first: + files.reverse() + result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files) + + if broken_first: + result.stdout.fnmatch_lines( + "*Error when collecting test, stopping test execution*" + ) + else: + result.stdout.fnmatch_lines("*errors during collection*") From bc345ac98033db5f5e65c299e43cfff88893f425 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Jun 2019 17:19:36 -0300 Subject: [PATCH 039/109] Remove handling of collection errors by --sw Since then pytest itself adopted the behavior of interrupting the test session on collection errors, so --sw no longer needs to handle this. The --sw behavior seems have been implemented when pytest would continue execution even if there were collection errors. --- src/_pytest/stepwise.py | 6 ------ testing/test_stepwise.py | 8 +------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 4a7e4d9b3fe..eb45554904a 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -70,12 +70,6 @@ def pytest_collection_modifyitems(self, session, config, items): config.hook.pytest_deselected(items=already_passed) - def pytest_collectreport(self, report): - if self.active and report.failed: - self.session.shouldstop = ( - "Error when collecting test, stopping test execution." - ) - def pytest_runtest_logreport(self, report): # Skip this hook if plugin is not active or the test is xfailed. if not self.active or "xfail" in report.keywords: diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 4b018c72222..a463b682859 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -164,10 +164,4 @@ def test_stop_on_collection_errors(broken_testdir, broken_first): if broken_first: files.reverse() result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files) - - if broken_first: - result.stdout.fnmatch_lines( - "*Error when collecting test, stopping test execution*" - ) - else: - result.stdout.fnmatch_lines("*errors during collection*") + result.stdout.fnmatch_lines("*errors during collection*") From 7513d87b153b67f6c1c6a8f491876bc0c7eb2e62 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Jun 2019 19:55:13 -0300 Subject: [PATCH 040/109] Remove broken/unused PytestPluginManager.addhooks The function has been deprecated for ages and the PLUGIN_MANAGER_ADDHOOKS constant doesn't even exist since 3.0. Because the function is clearly broken, this change doesn't even require a CHANGELOG. --- src/_pytest/config/__init__.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1f6ae98f9e3..c48688f3ae1 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -23,7 +23,6 @@ from .exceptions import UsageError from .findpaths import determine_setup from .findpaths import exists -from _pytest import deprecated from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest.outcomes import fail @@ -242,16 +241,6 @@ def __init__(self): # Used to know when we are importing conftests after the pytest_configure stage self._configured = False - def addhooks(self, module_or_class): - """ - .. deprecated:: 2.8 - - Use :py:meth:`pluggy.PluginManager.add_hookspecs ` - instead. - """ - warnings.warn(deprecated.PLUGIN_MANAGER_ADDHOOKS, stacklevel=2) - return self.add_hookspecs(module_or_class) - def parse_hookimpl_opts(self, plugin, name): # pytest hooks are always prefixed with pytest_ # so we avoid accessing possibly non-readable attributes From c94e9b61455ca868a8ca9443b851e97608ac5750 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Jun 2019 23:10:13 -0300 Subject: [PATCH 041/109] Fix test docstring --- testing/test_stepwise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index a463b682859..40c86fec3d8 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -158,8 +158,8 @@ def test_change_testfile(stepwise_testdir): @pytest.mark.parametrize("broken_first", [True, False]) def test_stop_on_collection_errors(broken_testdir, broken_first): - """Stop during collection errors. We have two possible messages depending on the order (#5444), - so test both cases.""" + """Stop during collection errors. Broken test first or broken test last + actually surfaced a bug (#5444), so we test both situations.""" files = ["working_testfile.py", "broken_testfile.py"] if broken_first: files.reverse() From 2b92fee1c315469c3f257673a508e51a1f1a6230 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 7 Jun 2019 12:58:51 +0200 Subject: [PATCH 042/109] initial conversion of exit codes to enum --- src/_pytest/config/__init__.py | 4 ++-- src/_pytest/main.py | 30 +++++++++++++++++------------- src/_pytest/pytester.py | 11 +++++------ src/_pytest/terminal.py | 18 +++++++----------- src/pytest.py | 2 ++ testing/acceptance_test.py | 29 ++++++++++++++--------------- testing/python/collect.py | 6 +++--- testing/test_assertrewrite.py | 6 +++--- testing/test_cacheprovider.py | 4 ++-- testing/test_capture.py | 4 ++-- testing/test_collection.py | 13 ++++++------- testing/test_config.py | 25 +++++++++++-------------- testing/test_conftest.py | 16 +++++++--------- testing/test_helpconfig.py | 8 ++++---- testing/test_mark.py | 4 ++-- testing/test_pluginmanager.py | 4 ++-- testing/test_pytester.py | 14 ++++++-------- testing/test_runner.py | 6 +++--- testing/test_session.py | 4 ++-- testing/test_terminal.py | 4 ++-- testing/test_unittest.py | 6 +++--- 21 files changed, 105 insertions(+), 113 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c48688f3ae1..e6de86c3619 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -48,7 +48,7 @@ def main(args=None, plugins=None): :arg plugins: list of plugin objects to be auto-registered during initialization. """ - from _pytest.main import EXIT_USAGEERROR + from _pytest.main import ExitCode try: try: @@ -78,7 +78,7 @@ def main(args=None, plugins=None): tw = py.io.TerminalWriter(sys.stderr) for msg in e.args: tw.line("ERROR: {}\n".format(msg), red=True) - return EXIT_USAGEERROR + return ExitCode.USAGE_ERROR class cmdline: # compatibility namespace diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 735d60bd68e..3aa36c80f14 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -1,4 +1,5 @@ """ core implementation of testing process: init, session, runtest loop. """ +import enum import fnmatch import functools import os @@ -19,12 +20,15 @@ from _pytest.runner import collect_one_node # exitcodes for the command line -EXIT_OK = 0 -EXIT_TESTSFAILED = 1 -EXIT_INTERRUPTED = 2 -EXIT_INTERNALERROR = 3 -EXIT_USAGEERROR = 4 -EXIT_NOTESTSCOLLECTED = 5 + + +class ExitCode(enum.IntEnum): + OK = 0 + TESTS_FAILED = 1 + INTERRUPTED = 2 + INTERNAL_ERROR = 3 + USAGE_ERROR = 4 + NO_TESTS_COLLECTED = 5 def pytest_addoption(parser): @@ -188,7 +192,7 @@ def pytest_configure(config): def wrap_session(config, doit): """Skeleton command line program""" session = Session(config) - session.exitstatus = EXIT_OK + session.exitstatus = ExitCode.OK initstate = 0 try: try: @@ -198,13 +202,13 @@ def wrap_session(config, doit): initstate = 2 session.exitstatus = doit(config, session) or 0 except UsageError: - session.exitstatus = EXIT_USAGEERROR + session.exitstatus = ExitCode.USAGE_ERROR raise except Failed: - session.exitstatus = EXIT_TESTSFAILED + session.exitstatus = ExitCode.TESTS_FAILED except (KeyboardInterrupt, exit.Exception): excinfo = _pytest._code.ExceptionInfo.from_current() - exitstatus = EXIT_INTERRUPTED + exitstatus = ExitCode.INTERRUPTED if isinstance(excinfo.value, exit.Exception): if excinfo.value.returncode is not None: exitstatus = excinfo.value.returncode @@ -217,7 +221,7 @@ def wrap_session(config, doit): except: # noqa excinfo = _pytest._code.ExceptionInfo.from_current() config.notify_exception(excinfo, config.option) - session.exitstatus = EXIT_INTERNALERROR + session.exitstatus = ExitCode.INTERNAL_ERROR if excinfo.errisinstance(SystemExit): sys.stderr.write("mainloop: caught unexpected SystemExit!\n") @@ -243,9 +247,9 @@ def _main(config, session): config.hook.pytest_runtestloop(session=session) if session.testsfailed: - return EXIT_TESTSFAILED + return ExitCode.TESTS_FAILED elif session.testscollected == 0: - return EXIT_NOTESTSCOLLECTED + return ExitCode.NO_TESTS_COLLECTED def pytest_collection(session): diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 4f0342ab5d4..1da5bd9ef9b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -19,8 +19,7 @@ from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.capture import MultiCapture from _pytest.capture import SysCapture -from _pytest.main import EXIT_INTERRUPTED -from _pytest.main import EXIT_OK +from _pytest.main import ExitCode from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import Path @@ -691,7 +690,7 @@ def getnode(self, config, arg): p = py.path.local(arg) config.hook.pytest_sessionstart(session=session) res = session.perform_collect([str(p)], genitems=False)[0] - config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) + config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res def getpathnode(self, path): @@ -708,11 +707,11 @@ def getpathnode(self, path): x = session.fspath.bestrelpath(path) config.hook.pytest_sessionstart(session=session) res = session.perform_collect([x], genitems=False)[0] - config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) + config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res def genitems(self, colitems): - """Generate all test items from a collection node. + """Generate all test items from a collection node.src/_pytest/main.py This recurses into the collection node and returns a list of all the test items contained within. @@ -841,7 +840,7 @@ class reprec: # typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing - if ret == EXIT_INTERRUPTED and not no_reraise_ctrlc: + if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc: calls = reprec.getcalls("pytest_keyboard_interrupt") if calls and calls[-1].excinfo.type == KeyboardInterrupt: raise KeyboardInterrupt() diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 6f989030175..91e37385276 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -16,11 +16,7 @@ import pytest from _pytest import nodes -from _pytest.main import EXIT_INTERRUPTED -from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.main import EXIT_OK -from _pytest.main import EXIT_TESTSFAILED -from _pytest.main import EXIT_USAGEERROR +from _pytest.main import ExitCode REPORT_COLLECTING_RESOLUTION = 0.5 @@ -654,17 +650,17 @@ def pytest_sessionfinish(self, exitstatus): outcome.get_result() self._tw.line("") summary_exit_codes = ( - EXIT_OK, - EXIT_TESTSFAILED, - EXIT_INTERRUPTED, - EXIT_USAGEERROR, - EXIT_NOTESTSCOLLECTED, + ExitCode.OK, + ExitCode.TESTS_FAILED, + ExitCode.INTERRUPTED, + ExitCode.USAGE_ERROR, + ExitCode.NO_TESTS_COLLECTED, ) if exitstatus in summary_exit_codes: self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config ) - if exitstatus == EXIT_INTERRUPTED: + if exitstatus == ExitCode.INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo self.summary_stats() diff --git a/src/pytest.py b/src/pytest.py index a6376843d24..a3fa260845d 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -15,6 +15,7 @@ from _pytest.fixtures import fixture from _pytest.fixtures import yield_fixture from _pytest.freeze_support import freeze_includes +from _pytest.main import ExitCode from _pytest.main import Session from _pytest.mark import MARK_GEN as mark from _pytest.mark import param @@ -57,6 +58,7 @@ "Collector", "deprecated_call", "exit", + "ExitCode", "fail", "File", "fixture", diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index caa0d5191f2..60cc21c4a01 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -8,8 +8,7 @@ import py import pytest -from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.main import EXIT_USAGEERROR +from _pytest.main import ExitCode from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG @@ -24,7 +23,7 @@ class TestGeneralUsage: def test_config_error(self, testdir): testdir.copy_example("conftest_usageerror/conftest.py") result = testdir.runpytest(testdir.tmpdir) - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines(["*ERROR: hello"]) result.stdout.fnmatch_lines(["*pytest_unconfigure_called"]) @@ -83,7 +82,7 @@ def pytest_unconfigure(): """ ) result = testdir.runpytest("-s", "asd") - assert result.ret == 4 # EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines(["ERROR: file not found*asd"]) result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"]) @@ -229,7 +228,7 @@ def pytest_collect_directory(): """ ) result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*1 skip*"]) def test_issue88_initial_file_multinodes(self, testdir): @@ -247,7 +246,7 @@ def test_issue93_initialnode_importing_capturing(self, testdir): """ ) result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED assert "should not be seen" not in result.stdout.str() assert "stderr42" not in result.stderr.str() @@ -290,13 +289,13 @@ def test_issue109_sibling_conftests_not_loaded(self, testdir): sub2 = testdir.mkdir("sub2") sub1.join("conftest.py").write("assert 0") result = testdir.runpytest(sub2) - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED sub2.ensure("__init__.py") p = sub2.ensure("test_hello.py") result = testdir.runpytest(p) - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result = testdir.runpytest(sub1) - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR def test_directory_skipped(self, testdir): testdir.makeconftest( @@ -308,7 +307,7 @@ def pytest_ignore_collect(): ) testdir.makepyfile("def test_hello(): pass") result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*1 skipped*"]) def test_multiple_items_per_collector_byid(self, testdir): @@ -410,10 +409,10 @@ def test_a(): def test_report_all_failed_collections_initargs(self, testdir): testdir.makeconftest( """ - from _pytest.main import EXIT_USAGEERROR + from _pytest.main import ExitCode def pytest_sessionfinish(exitstatus): - assert exitstatus == EXIT_USAGEERROR + assert exitstatus == ExitCode.USAGE_ERROR print("pytest_sessionfinish_called") """ ) @@ -421,7 +420,7 @@ def pytest_sessionfinish(exitstatus): result = testdir.runpytest("test_a.py::a", "test_b.py::b") result.stderr.fnmatch_lines(["*ERROR*test_a.py::a*", "*ERROR*test_b.py::b*"]) result.stdout.fnmatch_lines(["pytest_sessionfinish_called"]) - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR @pytest.mark.usefixtures("recwarn") def test_namespace_import_doesnt_confuse_import_hook(self, testdir): @@ -612,7 +611,7 @@ def test_invoke_with_invalid_type(self, capsys): def test_invoke_with_path(self, tmpdir, capsys): retcode = pytest.main(tmpdir) - assert retcode == EXIT_NOTESTSCOLLECTED + assert retcode == ExitCode.NO_TESTS_COLLECTED out, err = capsys.readouterr() def test_invoke_plugin_api(self, testdir, capsys): @@ -1160,7 +1159,7 @@ def test_fixture_mock_integration(testdir): def test_usage_error_code(testdir): result = testdir.runpytest("-unknown-option-") - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR @pytest.mark.filterwarnings("default") diff --git a/testing/python/collect.py b/testing/python/collect.py index d648452fd78..c2b09aeb86a 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -4,7 +4,7 @@ import _pytest._code import pytest -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode from _pytest.nodes import Collector @@ -246,7 +246,7 @@ def prop(self): """ ) result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED class TestFunction: @@ -1140,7 +1140,7 @@ class Test(object): ) result = testdir.runpytest() assert "TypeError" not in result.stdout.str() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_collect_functools_partial(testdir): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 61c5b93834f..0e6f42239f2 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -15,7 +15,7 @@ from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode def setup_module(mod): @@ -692,7 +692,7 @@ def test_zipfile(self, testdir): import test_gum.test_lizard""" % (z_fn,) ) - assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED + assert testdir.runpytest().ret == ExitCode.NO_TESTS_COLLECTED def test_readonly(self, testdir): sub = testdir.mkdir("testing") @@ -792,7 +792,7 @@ def test_package_without__init__py(self, testdir): pkg = testdir.mkdir("a_package_without_init_py") pkg.join("module.py").ensure() testdir.makepyfile("import a_package_without_init_py.module") - assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED + assert testdir.runpytest().ret == ExitCode.NO_TESTS_COLLECTED def test_rewrite_warning(self, testdir): testdir.makeconftest( diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index a2e701740a0..cbba27e5f5d 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -6,7 +6,7 @@ import py import pytest -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode pytest_plugins = ("pytester",) @@ -757,7 +757,7 @@ def test_2(): "* 2 deselected in *", ] ) - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_lastfailed_no_failures_behavior_empty_cache(self, testdir): testdir.makepyfile( diff --git a/testing/test_capture.py b/testing/test_capture.py index a85025c27d2..8d1d33bc736 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -12,7 +12,7 @@ import pytest from _pytest import capture from _pytest.capture import CaptureManager -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode # note: py.io capture tests where copied from # pylib 1.4.20.dev2 (rev 13d9af95547e) @@ -361,7 +361,7 @@ def test_conftestlogging_is_shown(self, testdir): ) # make sure that logging is still captured in tests result = testdir.runpytest_subprocess("-s", "-p", "no:capturelog") - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stderr.fnmatch_lines(["WARNING*hello435*"]) assert "operation on closed file" not in result.stderr.str() diff --git a/testing/test_collection.py b/testing/test_collection.py index c938a915d1d..864125c40af 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -7,8 +7,7 @@ import pytest from _pytest.main import _in_venv -from _pytest.main import EXIT_INTERRUPTED -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode from _pytest.main import Session @@ -347,7 +346,7 @@ def pytest_ignore_collect(path, config): assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*collected 0 items*"]) def test_collectignore_exclude_on_option(self, testdir): @@ -364,7 +363,7 @@ def pytest_configure(config): testdir.mkdir("hello") testdir.makepyfile(test_world="def test_hello(): pass") result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED assert "passed" not in result.stdout.str() result = testdir.runpytest("--XX") assert result.ret == 0 @@ -384,7 +383,7 @@ def pytest_configure(config): testdir.makepyfile(test_world="def test_hello(): pass") testdir.makepyfile(test_welt="def test_hallo(): pass") result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*collected 0 items*"]) result = testdir.runpytest("--XX") assert result.ret == 0 @@ -1172,7 +1171,7 @@ def test_collectignore_via_conftest(testdir, monkeypatch): ignore_me.ensure("conftest.py").write("assert 0, 'should_not_be_called'") result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_collect_pkg_init_and_file_in_args(testdir): @@ -1234,7 +1233,7 @@ def test_collect_sub_with_symlinks(use_pkg, testdir): def test_collector_respects_tbstyle(testdir): p1 = testdir.makepyfile("assert 0") result = testdir.runpytest(p1, "--tb=native") - assert result.ret == EXIT_INTERRUPTED + assert result.ret == ExitCode.INTERRUPTED result.stdout.fnmatch_lines( [ "*_ ERROR collecting test_collector_respects_tbstyle.py _*", diff --git a/testing/test_config.py b/testing/test_config.py index c3b027ab901..b9fc388d236 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -10,10 +10,7 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg -from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.main import EXIT_OK -from _pytest.main import EXIT_TESTSFAILED -from _pytest.main import EXIT_USAGEERROR +from _pytest.main import ExitCode class TestParseIni: @@ -189,7 +186,7 @@ def test_absolute_win32_path(self, testdir): temp_ini_file = normpath(str(temp_ini_file)) ret = pytest.main(["-c", temp_ini_file]) - assert ret == _pytest.main.EXIT_OK + assert ret == ExitCode.OK class TestConfigAPI: @@ -726,7 +723,7 @@ def test_consider_args_after_options_for_rootdir(testdir, args): @pytest.mark.skipif("sys.platform == 'win32'") def test_toolongargs_issue224(testdir): result = testdir.runpytest("-m", "hello" * 500) - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_config_in_subdirectory_colon_command_line_issue2148(testdir): @@ -1086,7 +1083,7 @@ def test_addopts_from_ini_not_concatenated(self, testdir): % (testdir.request.config._parser.optparser.prog,) ] ) - assert result.ret == _pytest.main.EXIT_USAGEERROR + assert result.ret == _pytest.main.ExitCode.USAGE_ERROR def test_override_ini_does_not_contain_paths(self, _config_for_test, _sys_snapshot): """Check that -o no longer swallows all options after it (#3103)""" @@ -1175,13 +1172,13 @@ def pytest_addoption(parser): ) # Does not display full/default help. assert "to see available markers type: pytest --markers" not in result.stdout.lines - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR result = testdir.runpytest("--version") result.stderr.fnmatch_lines( ["*pytest*{}*imported from*".format(pytest.__version__)] ) - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR def test_config_does_not_load_blocked_plugin_from_args(testdir): @@ -1189,11 +1186,11 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): p = testdir.makepyfile("def test(capfd): pass") result = testdir.runpytest(str(p), "-pno:capture") result.stdout.fnmatch_lines(["E fixture 'capfd' not found"]) - assert result.ret == EXIT_TESTSFAILED + assert result.ret == ExitCode.TESTS_FAILED result = testdir.runpytest(str(p), "-pno:capture", "-s") result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR @pytest.mark.parametrize( @@ -1219,7 +1216,7 @@ def test_config_blocked_default_plugins(testdir, plugin): result = testdir.runpytest(str(p), "-pno:%s" % plugin) if plugin == "python": - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines( [ "ERROR: not found: */test_config_blocked_default_plugins.py", @@ -1228,13 +1225,13 @@ def test_config_blocked_default_plugins(testdir, plugin): ) return - assert result.ret == EXIT_OK + assert result.ret == ExitCode.OK if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 passed in *"]) p = testdir.makepyfile("def test(): assert 0") result = testdir.runpytest(str(p), "-pno:%s" % plugin) - assert result.ret == EXIT_TESTSFAILED + assert result.ret == ExitCode.TESTS_FAILED if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 failed in *"]) else: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index f29531eb7bd..447416f1076 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -4,9 +4,7 @@ import pytest from _pytest.config import PytestPluginManager -from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.main import EXIT_OK -from _pytest.main import EXIT_USAGEERROR +from _pytest.main import ExitCode def ConftestWithSetinitial(path): @@ -223,11 +221,11 @@ def fixture(): "PASSED", ] ) - assert result.ret == EXIT_OK + assert result.ret == ExitCode.OK # Should not cause "ValueError: Plugin already registered" (#4174). result = testdir.runpytest("-vs", "symlink") - assert result.ret == EXIT_OK + assert result.ret == ExitCode.OK realtests.ensure("__init__.py") result = testdir.runpytest("-vs", "symlinktests/test_foo.py::test1") @@ -238,7 +236,7 @@ def fixture(): "PASSED", ] ) - assert result.ret == EXIT_OK + assert result.ret == ExitCode.OK @pytest.mark.skipif( @@ -274,16 +272,16 @@ def fixture(): build.chdir() result = testdir.runpytest("-vs", "app/test_foo.py") result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) - assert result.ret == EXIT_OK + assert result.ret == ExitCode.OK def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result = testdir.runpytest() - assert result.ret == EXIT_USAGEERROR + assert result.ret == ExitCode.USAGE_ERROR def test_conftest_existing_resultlog(testdir): diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index ec061cad99d..962750f7b3a 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -1,5 +1,5 @@ import pytest -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode def test_version(testdir, pytestconfig): @@ -49,7 +49,7 @@ def pytest_hello(xyz): """ ) result = testdir.runpytest() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_traceconfig(testdir): @@ -59,7 +59,7 @@ def test_traceconfig(testdir): def test_debug(testdir, monkeypatch): result = testdir.runpytest_subprocess("--debug") - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED p = testdir.tmpdir.join("pytestdebug.log") assert "pytest_sessionstart" in p.read() @@ -67,7 +67,7 @@ def test_debug(testdir, monkeypatch): def test_PYTEST_DEBUG(testdir, monkeypatch): monkeypatch.setenv("PYTEST_DEBUG", "1") result = testdir.runpytest_subprocess() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stderr.fnmatch_lines( ["*pytest_plugin_registered*", "*manager*PluginManager*"] ) diff --git a/testing/test_mark.py b/testing/test_mark.py index 8d97f8b4ef5..c22e9dbb5de 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -3,7 +3,7 @@ from unittest import mock import pytest -from _pytest.main import EXIT_INTERRUPTED +from _pytest.main import ExitCode from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark from _pytest.nodes import Collector @@ -903,7 +903,7 @@ def test(): "*= 1 error in *", ] ) - assert result.ret == EXIT_INTERRUPTED + assert result.ret == ExitCode.INTERRUPTED def test_parameterset_for_parametrize_bad_markname(testdir): diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 8afb37fa149..97f220ca553 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -5,7 +5,7 @@ import pytest from _pytest.config import PytestPluginManager from _pytest.config.exceptions import UsageError -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode from _pytest.main import Session @@ -227,7 +227,7 @@ def test_plugin_skip(self, testdir, monkeypatch): p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines( ["*skipped plugin*skipping1*hello*", "*skipped plugin*skipping2*hello*"] ) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index ca12672f598..37b63f31afe 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -8,9 +8,7 @@ import _pytest.pytester as pytester import pytest from _pytest.config import PytestPluginManager -from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.main import EXIT_OK -from _pytest.main import EXIT_TESTSFAILED +from _pytest.main import ExitCode from _pytest.pytester import CwdSnapshot from _pytest.pytester import HookRecorder from _pytest.pytester import LineMatcher @@ -206,11 +204,11 @@ class TestInlineRunModulesCleanup: def test_inline_run_test_module_not_cleaned_up(self, testdir): test_mod = testdir.makepyfile("def test_foo(): assert True") result = testdir.inline_run(str(test_mod)) - assert result.ret == EXIT_OK + assert result.ret == ExitCode.OK # rewrite module, now test should fail if module was re-imported test_mod.write("def test_foo(): assert False") result2 = testdir.inline_run(str(test_mod)) - assert result2.ret == EXIT_TESTSFAILED + assert result2.ret == ExitCode.TESTS_FAILED def spy_factory(self): class SysModulesSnapshotSpy: @@ -411,12 +409,12 @@ def test_testdir_subprocess(testdir): def test_unicode_args(testdir): result = testdir.runpytest("-k", "💩") - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_testdir_run_no_timeout(testdir): testfile = testdir.makepyfile("def test_no_timeout(): pass") - assert testdir.runpytest_subprocess(testfile).ret == EXIT_OK + assert testdir.runpytest_subprocess(testfile).ret == ExitCode.OK def test_testdir_run_with_timeout(testdir): @@ -429,7 +427,7 @@ def test_testdir_run_with_timeout(testdir): end = time.time() duration = end - start - assert result.ret == EXIT_OK + assert result.ret == ExitCode.OK assert duration < timeout diff --git a/testing/test_runner.py b/testing/test_runner.py index 13f722036dc..15180c07156 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -653,7 +653,7 @@ def test_hello(): def test_pytest_no_tests_collected_exit_status(testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(["*collected 0 items*"]) - assert result.ret == main.EXIT_NOTESTSCOLLECTED + assert result.ret == main.ExitCode.NO_TESTS_COLLECTED testdir.makepyfile( test_foo=""" @@ -664,12 +664,12 @@ def test_foo(): result = testdir.runpytest() result.stdout.fnmatch_lines(["*collected 1 item*"]) result.stdout.fnmatch_lines(["*1 passed*"]) - assert result.ret == main.EXIT_OK + assert result.ret == main.ExitCode.OK result = testdir.runpytest("-k nonmatch") result.stdout.fnmatch_lines(["*collected 1 item*"]) result.stdout.fnmatch_lines(["*1 deselected*"]) - assert result.ret == main.EXIT_NOTESTSCOLLECTED + assert result.ret == main.ExitCode.NO_TESTS_COLLECTED def test_exception_printing_skip(): diff --git a/testing/test_session.py b/testing/test_session.py index 1a0ae808076..dbe0573760b 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -1,5 +1,5 @@ import pytest -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode class SessionTests: @@ -330,7 +330,7 @@ def pytest_sessionfinish(): """ ) res = testdir.runpytest("--collect-only") - assert res.ret == EXIT_NOTESTSCOLLECTED + assert res.ret == ExitCode.NO_TESTS_COLLECTED @pytest.mark.parametrize("path", ["root", "{relative}/root", "{environment}/root"]) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cc2c474ab45..bf029fbc533 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -10,7 +10,7 @@ import py import pytest -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode from _pytest.reports import BaseReport from _pytest.terminal import _folded_skips from _pytest.terminal import _get_line_with_reprcrash_message @@ -937,7 +937,7 @@ def test_opt(arg): def test_traceconfig(testdir, monkeypatch): result = testdir.runpytest("--traceconfig") result.stdout.fnmatch_lines(["*active plugins*"]) - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED class TestGenericReporting: diff --git a/testing/test_unittest.py b/testing/test_unittest.py index adcc232fd98..2467ddd39db 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1,7 +1,7 @@ import gc import pytest -from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import ExitCode def test_simple_unittest(testdir): @@ -55,7 +55,7 @@ def __getattr__(self, tag): """ ) result = testdir.runpytest(testpath) - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_setup(testdir): @@ -704,7 +704,7 @@ class Test(unittest.TestCase): ) result = testdir.runpytest() assert "TypeError" not in result.stdout.str() - assert result.ret == EXIT_NOTESTSCOLLECTED + assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_unittest_typerror_traceback(testdir): From 2bd619ecb052579fd2ae71000125826a410159a1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 9 Jun 2019 10:35:55 +0200 Subject: [PATCH 043/109] add changelog --- changelog/5125.removal.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/5125.removal.rst diff --git a/changelog/5125.removal.rst b/changelog/5125.removal.rst new file mode 100644 index 00000000000..73616caebc7 --- /dev/null +++ b/changelog/5125.removal.rst @@ -0,0 +1,2 @@ +Introduce the ``pytest.ExitCode`` Enum and make session.exitcode an instance of it. +User defined exit codes are still valid, but consumers need to take the enum into account. From 355eb5adfbe795d50d7d60fddef42d7e3cccb02e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 15 Jun 2019 10:06:37 -0300 Subject: [PATCH 044/109] Small cleanups on _pytest.compat Small improvements and cleanups --- src/_pytest/compat.py | 29 +++++++++++++++-------------- src/_pytest/fixtures.py | 8 +------- src/_pytest/logging.py | 6 +++--- src/_pytest/python.py | 6 ++---- src/_pytest/python_api.py | 6 ++++-- 5 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 6f1275e61cc..2c964f473aa 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,6 +10,7 @@ from inspect import Parameter from inspect import signature +import attr import py import _pytest @@ -29,10 +30,6 @@ def _format_args(func): return str(signature(func)) -isfunction = inspect.isfunction -isclass = inspect.isclass -# used to work around a python2 exception info leak -exc_clear = getattr(sys, "exc_clear", lambda: None) # The type of re.compile objects is not exposed in Python. REGEX_TYPE = type(re.compile("")) @@ -129,11 +126,15 @@ def getfuncargnames(function, is_method=False, cls=None): return arg_names -@contextmanager -def dummy_context_manager(): - """Context manager that does nothing, useful in situations where you might need an actual context manager or not - depending on some condition. Using this allow to keep the same code""" - yield +if sys.version_info < (3, 7): + + @contextmanager + def nullcontext(): + yield + + +else: + from contextlib import nullcontext # noqa def get_default_arg_names(function): @@ -191,6 +192,7 @@ def ascii_escaped(val): return _translate_non_printable(ret) +@attr.s class _PytestWrapper: """Dummy wrapper around a function object for internal use only. @@ -199,8 +201,7 @@ class _PytestWrapper: to issue warnings when the fixture function is called directly. """ - def __init__(self, obj): - self.obj = obj + obj = attr.ib() def get_real_func(obj): @@ -280,7 +281,7 @@ def safe_getattr(object, name, default): def safe_isclass(obj): """Ignore any exception via isinstance on Python 3.""" try: - return isclass(obj) + return inspect.isclass(obj) except Exception: return False @@ -304,8 +305,8 @@ def _setup_collect_fakemodule(): pytest.collect = ModuleType("pytest.collect") pytest.collect.__all__ = [] # used for setns - for attr in COLLECT_FAKEMODULE_ATTRIBUTES: - setattr(pytest.collect, attr, getattr(pytest, attr)) + for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES: + setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) class CaptureIO(io.TextIOWrapper): diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 2f9b10b85d8..0a792d11d2c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,7 +16,6 @@ from _pytest._code.code import TerminalRepr from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper -from _pytest.compat import exc_clear from _pytest.compat import FuncargnamesCompatAttr from _pytest.compat import get_real_func from _pytest.compat import get_real_method @@ -25,7 +24,6 @@ from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator -from _pytest.compat import isclass from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.deprecated import FIXTURE_FUNCTION_CALL @@ -572,10 +570,6 @@ def _compute_fixture_value(self, fixturedef): # check if a higher-level scoped fixture accesses a lower level one subrequest._check_scope(argname, self.scope, scope) - - # clear sys.exc_info before invoking the fixture (python bug?) - # if it's not explicitly cleared it will leak into the call - exc_clear() try: # call the fixture function fixturedef.execute(request=subrequest) @@ -970,7 +964,7 @@ class FixtureFunctionMarker: name = attr.ib(default=None) def __call__(self, function): - if isclass(function): + if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") if getattr(function, "_pytestfixturefunction", False): diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index df18b81cdc0..2861baefda3 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -6,7 +6,7 @@ import py import pytest -from _pytest.compat import dummy_context_manager +from _pytest.compat import nullcontext from _pytest.config import create_terminal_writer from _pytest.pathlib import Path @@ -436,7 +436,7 @@ def __init__(self, config): self.log_cli_handler = None - self.live_logs_context = lambda: dummy_context_manager() + self.live_logs_context = lambda: nullcontext() # Note that the lambda for the live_logs_context is needed because # live_logs_context can otherwise not be entered multiple times due # to limitations of contextlib.contextmanager. @@ -676,7 +676,7 @@ def emit(self, record): ctx_manager = ( self.capture_manager.global_and_fixture_disabled() if self.capture_manager - else dummy_context_manager() + else nullcontext() ) with ctx_manager: if not self._first_record_emitted: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index b4d8f5ae0b0..9b2d467dca5 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -23,8 +23,6 @@ from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator -from _pytest.compat import isclass -from _pytest.compat import isfunction from _pytest.compat import NOTSET from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr @@ -207,7 +205,7 @@ def pytest_pycollect_makeitem(collector, name, obj): # We need to try and unwrap the function if it's a functools.partial # or a funtools.wrapped. # We musn't if it's been wrapped with mock.patch (python 2 only) - if not (isfunction(obj) or isfunction(get_real_func(obj))): + if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))): filename, lineno = getfslineno(obj) warnings.warn_explicit( message=PytestCollectionWarning( @@ -1190,7 +1188,7 @@ def _idval(val, argname, idx, idfn, item, config): return ascii_escaped(val.pattern) elif enum is not None and isinstance(val, enum.Enum): return str(val) - elif (isclass(val) or isfunction(val)) and hasattr(val, "__name__"): + elif (inspect.isclass(val) or inspect.isfunction(val)) and hasattr(val, "__name__"): return val.__name__ return str(argname) + str(idx) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 011181a4071..35fd732a71f 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,3 +1,4 @@ +import inspect import math import pprint import sys @@ -13,7 +14,6 @@ import _pytest._code from _pytest import deprecated -from _pytest.compat import isclass from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail @@ -658,7 +658,9 @@ def raises(expected_exception, *args, **kwargs): """ __tracebackhide__ = True - for exc in filterfalse(isclass, always_iterable(expected_exception, BASE_TYPE)): + for exc in filterfalse( + inspect.isclass, always_iterable(expected_exception, BASE_TYPE) + ): msg = ( "exceptions must be old-style classes or" " derived from BaseException, not %s" From 177af032d237f0a352a3655e8084b12289fbac1e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 15 Jun 2019 10:16:59 -0300 Subject: [PATCH 045/109] Remove unused/unneeded code --- src/_pytest/python.py | 2 -- src/_pytest/python_api.py | 14 -------------- testing/python/metafunc.py | 2 +- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9b2d467dca5..66d8530602b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1170,8 +1170,6 @@ def _idval(val, argname, idx, idfn, item, config): # See issue https://github.com/pytest-dev/pytest/issues/2169 msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n" msg = msg.format(item.nodeid, argname, idx) - # we only append the exception type and message because on Python 2 reraise does nothing - msg += " {}: {}\n".format(type(e).__name__, e) raise ValueError(msg) from e elif config: hook_id = config.hook.pytest_make_parametrize_id( diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 35fd732a71f..374fa598fb6 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -20,20 +20,6 @@ BASE_TYPE = (type, STRING_TYPES) -def _cmp_raises_type_error(self, other): - """__cmp__ implementation which raises TypeError. Used - by Approx base classes to implement only == and != and raise a - TypeError for other comparisons. - - Needed in Python 2 only, Python 3 all it takes is not implementing the - other operators at all. - """ - __tracebackhide__ = True - raise TypeError( - "Comparison operators other than == and != not supported by approx objects" - ) - - def _non_numeric_type_error(value, at): at_str = " at {}".format(at) if at else "" return TypeError( diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 4702f0b57d2..6b26be72b89 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -506,8 +506,8 @@ def test_foo(arg): result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "*test_foo: error raised while trying to determine id of parameter 'arg' at position 0", "*Exception: bad ids", + "*test_foo: error raised while trying to determine id of parameter 'arg' at position 0", ] ) From d8fa434d3980846acf8917325c49be5a85e4b5a1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 15 Jun 2019 10:18:44 -0300 Subject: [PATCH 046/109] Remove Python 2-only workaround --- src/_pytest/assertion/util.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index f2cb2ab6374..762e5761d71 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -244,17 +244,9 @@ def _compare_eq_iterable(left, right, verbose=0): # dynamic import to speedup pytest import difflib - try: - left_formatting = pprint.pformat(left).splitlines() - right_formatting = pprint.pformat(right).splitlines() - explanation = ["Full diff:"] - except Exception: - # hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling - # sorted() on a list would raise. See issue #718. - # As a workaround, the full diff is generated by using the repr() string of each item of each container. - left_formatting = sorted(repr(x) for x in left) - right_formatting = sorted(repr(x) for x in right) - explanation = ["Full diff (fallback to calling repr on each item):"] + left_formatting = pprint.pformat(left).splitlines() + right_formatting = pprint.pformat(right).splitlines() + explanation = ["Full diff:"] explanation.extend( line.strip() for line in difflib.ndiff(left_formatting, right_formatting) ) From 43e8576ca3403484f65a603bd748576e21d57f11 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 15 Jun 2019 11:40:08 -0300 Subject: [PATCH 047/109] Make pytest warnings show as from 'pytest' module instead of '_pytest.warning_types' When we configure warnings as errors, users see error messages like this: def test(): > warnings.warn(pytest.PytestWarning("some warning")) E _pytest.warning_types.PytestWarning: some warning This is a problem because suggests the user should use `_pytest.warning_types.PytestWarning` to configure their warning filters, which is not nice. This commit changes the message to: def test(): > warnings.warn(pytest.PytestWarning("some warning")) E pytest.PytestWarning: some warning --- changelog/5452.feature.rst | 1 + src/_pytest/warning_types.py | 20 +++++++++++++++++++ testing/test_warning_types.py | 37 +++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 changelog/5452.feature.rst create mode 100644 testing/test_warning_types.py diff --git a/changelog/5452.feature.rst b/changelog/5452.feature.rst new file mode 100644 index 00000000000..4e47e971ea7 --- /dev/null +++ b/changelog/5452.feature.rst @@ -0,0 +1 @@ +When warnings are configured as errors, pytest warnings now appear as originating from ``pytest.`` instead of the internal ``_pytest.warning_types.`` module. diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index d7d37b4bbe0..ac7e5ca48b4 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -8,6 +8,8 @@ class PytestWarning(UserWarning): Base class for all warnings emitted by pytest. """ + __module__ = "pytest" + class PytestAssertRewriteWarning(PytestWarning): """ @@ -16,6 +18,8 @@ class PytestAssertRewriteWarning(PytestWarning): Warning emitted by the pytest assert rewrite module. """ + __module__ = "pytest" + class PytestCacheWarning(PytestWarning): """ @@ -24,6 +28,8 @@ class PytestCacheWarning(PytestWarning): Warning emitted by the cache plugin in various situations. """ + __module__ = "pytest" + class PytestConfigWarning(PytestWarning): """ @@ -32,6 +38,8 @@ class PytestConfigWarning(PytestWarning): Warning emitted for configuration issues. """ + __module__ = "pytest" + class PytestCollectionWarning(PytestWarning): """ @@ -40,6 +48,8 @@ class PytestCollectionWarning(PytestWarning): Warning emitted when pytest is not able to collect a file or symbol in a module. """ + __module__ = "pytest" + class PytestDeprecationWarning(PytestWarning, DeprecationWarning): """ @@ -48,6 +58,8 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning): Warning class for features that will be removed in a future version. """ + __module__ = "pytest" + class PytestExperimentalApiWarning(PytestWarning, FutureWarning): """ @@ -57,6 +69,8 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): removed completely in future version """ + __module__ = "pytest" + @classmethod def simple(cls, apiname): return cls( @@ -75,6 +89,8 @@ class PytestUnhandledCoroutineWarning(PytestWarning): are not natively supported. """ + __module__ = "pytest" + class PytestUnknownMarkWarning(PytestWarning): """ @@ -84,6 +100,8 @@ class PytestUnknownMarkWarning(PytestWarning): See https://docs.pytest.org/en/latest/mark.html for details. """ + __module__ = "pytest" + class RemovedInPytest4Warning(PytestDeprecationWarning): """ @@ -92,6 +110,8 @@ class RemovedInPytest4Warning(PytestDeprecationWarning): Warning class for features scheduled to be removed in pytest 4.0. """ + __module__ = "pytest" + @attr.s class UnformattedWarning: diff --git a/testing/test_warning_types.py b/testing/test_warning_types.py new file mode 100644 index 00000000000..f16d7252a68 --- /dev/null +++ b/testing/test_warning_types.py @@ -0,0 +1,37 @@ +import inspect + +import _pytest.warning_types +import pytest + + +@pytest.mark.parametrize( + "warning_class", + [ + w + for n, w in vars(_pytest.warning_types).items() + if inspect.isclass(w) and issubclass(w, Warning) + ], +) +def test_warning_types(warning_class): + """Make sure all warnings declared in _pytest.warning_types are displayed as coming + from 'pytest' instead of the internal module (#5452). + """ + assert warning_class.__module__ == "pytest" + + +@pytest.mark.filterwarnings("error::pytest.PytestWarning") +def test_pytest_warnings_repr_integration_test(testdir): + """Small integration test to ensure our small hack of setting the __module__ attribute + of our warnings actually works (#5452). + """ + testdir.makepyfile( + """ + import pytest + import warnings + + def test(): + warnings.warn(pytest.PytestWarning("some warning")) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["E pytest.PytestWarning: some warning"]) From 065fa17124d041092381a14566e5ae5ff1fdc5c6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 15 Jun 2019 17:03:40 +0200 Subject: [PATCH 048/109] update cangelog to fit review suggestion --- changelog/5125.removal.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog/5125.removal.rst b/changelog/5125.removal.rst index 73616caebc7..0102d6305f5 100644 --- a/changelog/5125.removal.rst +++ b/changelog/5125.removal.rst @@ -1,2 +1,5 @@ -Introduce the ``pytest.ExitCode`` Enum and make session.exitcode an instance of it. -User defined exit codes are still valid, but consumers need to take the enum into account. +``Session.exitcode`` values are now coded in ``pytest.ExitCode``, an ``IntEnum``. This makes the exit code available for consumer code and are more explicit other than just documentation. User defined exit codes are still valid, but should be used with caution. + +The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios. + +**pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change. \ No newline at end of file From 103d6146b0921c73465129e4eede51cd2f531779 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 15 Jun 2019 17:18:21 +0200 Subject: [PATCH 049/109] document exitcode members --- doc/en/reference.rst | 8 ++++++++ src/_pytest/main.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 0b168eb5442..3b1d3f2620d 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -727,6 +727,14 @@ ExceptionInfo .. autoclass:: _pytest._code.ExceptionInfo :members: + +pytest.ExitCode +~~~~~~~~~~~~~~~ + +.. autoclass:: _pytest.main.ExitCode + :members: + + FixtureDef ~~~~~~~~~~ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 3aa36c80f14..3d0d95e8d2c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -19,15 +19,23 @@ from _pytest.outcomes import exit from _pytest.runner import collect_one_node -# exitcodes for the command line - class ExitCode(enum.IntEnum): + """ + encodes the valid exit codes of pytest + currently users may still supply other exit codes as well + """ + #: tests passed OK = 0 + #: tests failed TESTS_FAILED = 1 + #: pytest was interrupted INTERRUPTED = 2 + #: an internal error got in the way INTERNAL_ERROR = 3 + #: pytest was missused USAGE_ERROR = 4 + #: pytest couldnt find tests NO_TESTS_COLLECTED = 5 From 8b3b10b14b01598b9b778c5459b67c4694ac13d3 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 15 Jun 2019 17:41:13 +0200 Subject: [PATCH 050/109] pre-commit --- changelog/5125.removal.rst | 2 +- src/_pytest/main.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/5125.removal.rst b/changelog/5125.removal.rst index 0102d6305f5..8f67c7399ae 100644 --- a/changelog/5125.removal.rst +++ b/changelog/5125.removal.rst @@ -2,4 +2,4 @@ The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios. -**pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change. \ No newline at end of file +**pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 3d0d95e8d2c..1a92239e2de 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -25,6 +25,7 @@ class ExitCode(enum.IntEnum): encodes the valid exit codes of pytest currently users may still supply other exit codes as well """ + #: tests passed OK = 0 #: tests failed From 1cfea5f1b33272a332a6b87c9ae99892be00e2c0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 15 Jun 2019 21:41:55 +0200 Subject: [PATCH 051/109] add ExitCode reference in usage --- doc/en/usage.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/en/usage.rst b/doc/en/usage.rst index acf736f211e..ad7e2cdabc5 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -33,6 +33,8 @@ Running ``pytest`` can result in six different exit codes: :Exit code 4: pytest command line usage error :Exit code 5: No tests were collected +They are repressended by the :class:`_pytest.main.ExitCode` enum. + Getting help on version, option names, environment variables -------------------------------------------------------------- From ab6ed381ac336baafc26175e68645f7e4e5d88ae Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 15 Jun 2019 20:53:46 -0300 Subject: [PATCH 052/109] Improve ExitCode docstring --- src/_pytest/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1a92239e2de..e9b31bca7cf 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -22,8 +22,9 @@ class ExitCode(enum.IntEnum): """ - encodes the valid exit codes of pytest - currently users may still supply other exit codes as well + Encodes the valid exit codes by pytest. + + Currently users and plugins may supply other exit codes as well. """ #: tests passed From 0627d92df248a3263234d0a560c18fc711584c81 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 17 Jun 2019 20:35:23 +0200 Subject: [PATCH 053/109] fix typos in the resolution of #5125 --- doc/en/usage.rst | 2 +- src/_pytest/main.py | 4 ++-- src/_pytest/pytester.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/en/usage.rst b/doc/en/usage.rst index ad7e2cdabc5..0d464e20799 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -33,7 +33,7 @@ Running ``pytest`` can result in six different exit codes: :Exit code 4: pytest command line usage error :Exit code 5: No tests were collected -They are repressended by the :class:`_pytest.main.ExitCode` enum. +They are represented by the :class:`_pytest.main.ExitCode` enum. Getting help on version, option names, environment variables -------------------------------------------------------------- diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e9b31bca7cf..118fc784a92 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -35,9 +35,9 @@ class ExitCode(enum.IntEnum): INTERRUPTED = 2 #: an internal error got in the way INTERNAL_ERROR = 3 - #: pytest was missused + #: pytest was misused USAGE_ERROR = 4 - #: pytest couldnt find tests + #: pytest couldn't find tests NO_TESTS_COLLECTED = 5 diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 1da5bd9ef9b..6b304ad9ff1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -711,7 +711,7 @@ def getpathnode(self, path): return res def genitems(self, colitems): - """Generate all test items from a collection node.src/_pytest/main.py + """Generate all test items from a collection node. This recurses into the collection node and returns a list of all the test items contained within. From 12b76b626104d68c72a06557387deeb52b819508 Mon Sep 17 00:00:00 2001 From: curiousjazz77 <10506008+curiousjazz77@users.noreply.github.com> Date: Fri, 21 Jun 2019 15:48:59 -0700 Subject: [PATCH 054/109] Update cache.rst --- doc/en/cache.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 27a8449108c..c77b985b075 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -113,8 +113,8 @@ If you then run it with ``--lf``: test_50.py:6: Failed ================= 2 failed, 48 deselected in 0.12 seconds ================== -You have run only the two failing test from the last run, while 48 tests have -not been run ("deselected"). +You have run only the two failing tests from the last run, while the 48 passing +tests have not been run ("deselected"). Now, if you run with the ``--ff`` option, all tests will be run but the first previous failures will be executed first (as can be seen from the series @@ -224,7 +224,7 @@ If you run this command for the first time, you can see the print statement: running expensive computation... 1 failed in 0.12 seconds -If you run it a second time the value will be retrieved from +If you run it a second time, the value will be retrieved from the cache and nothing will be printed: .. code-block:: pytest From a37b902afea21621639b114f087e84f70fb057ba Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 12 Jun 2019 18:49:51 -0300 Subject: [PATCH 055/109] Integrate pytest-faulthandler into the core * Add pytest-faulthandler files unchanged * Adapt imports and tests * Add code to skip registration of the external `pytest_faulthandler` to avoid conflicts Fix #5440 --- changelog/5440.feature.rst | 8 +++ doc/en/usage.rst | 19 +++++- src/_pytest/config/__init__.py | 3 +- src/_pytest/deprecated.py | 8 +++ src/_pytest/faulthandler.py | 102 +++++++++++++++++++++++++++++++++ testing/deprecated_test.py | 23 +++----- testing/test_faulthandler.py | 99 ++++++++++++++++++++++++++++++++ 7 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 changelog/5440.feature.rst create mode 100644 src/_pytest/faulthandler.py create mode 100644 testing/test_faulthandler.py diff --git a/changelog/5440.feature.rst b/changelog/5440.feature.rst new file mode 100644 index 00000000000..d3bb95f5841 --- /dev/null +++ b/changelog/5440.feature.rst @@ -0,0 +1,8 @@ +The `faulthandler `__ standard library +module is now enabled by default to help users diagnose crashes in C modules. + +This functionality was provided by integrating the external +`pytest-faulthandler `__ plugin into the core, +so users should remove that plugin from their requirements if used. + +For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler diff --git a/doc/en/usage.rst b/doc/en/usage.rst index acf736f211e..c1332706fc7 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -408,7 +408,6 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours: Profiling test execution duration ------------------------------------- -.. versionadded: 2.2 To get a list of the slowest 10 test durations: @@ -418,6 +417,24 @@ To get a list of the slowest 10 test durations: By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line. + +.. _faulthandler: + +Fault Handler +------------- + +.. versionadded:: 5.0 + +The `faulthandler `__ standard module +can be used to dump Python tracebacks on a segfault or after a timeout. + +The module is automatically enabled for pytest runs, unless the ``--no-faulthandler`` is given +on the command-line. + +Also the ``--faulthandler-timeout=X`` can be used to dump the traceback of all threads if a test +takes longer than ``X`` seconds to finish (not available on Windows). + + Creating JUnitXML format files ---------------------------------------------------- diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1f6ae98f9e3..74ee4a2bc80 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -141,6 +141,7 @@ def directory_arg(path, optname): "warnings", "logging", "reports", + "faulthandler", ) builtin_plugins = set(default_plugins) @@ -299,7 +300,7 @@ def parse_hookspec_opts(self, module_or_class, name): return opts def register(self, plugin, name=None): - if name in ["pytest_catchlog", "pytest_capturelog"]: + if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: warnings.warn( PytestConfigWarning( "{} plugin has been merged into the core, " diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 3feae8b4346..1c544fd3681 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -14,6 +14,14 @@ YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" +# set of plugins which have been integrated into the core; we use this list to ignore +# them during registration to avoid conflicts +DEPRECATED_EXTERNAL_PLUGINS = { + "pytest_catchlog", + "pytest_capturelog", + "pytest_faulthandler", +} + FIXTURE_FUNCTION_CALL = ( 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py new file mode 100644 index 00000000000..48fe0f218d5 --- /dev/null +++ b/src/_pytest/faulthandler.py @@ -0,0 +1,102 @@ +import io +import os +import sys + +import pytest + + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting") + group.addoption( + "--no-faulthandler", + action="store_false", + dest="fault_handler", + default=True, + help="Disable faulthandler module.", + ) + + group.addoption( + "--faulthandler-timeout", + type=float, + dest="fault_handler_timeout", + metavar="TIMEOUT", + default=0.0, + help="Dump the traceback of all threads if a test takes " + "more than TIMEOUT seconds to finish.\n" + "Not available on Windows.", + ) + + +def pytest_configure(config): + if config.getoption("fault_handler"): + import faulthandler + + # avoid trying to dup sys.stderr if faulthandler is already enabled + if faulthandler.is_enabled(): + return + + stderr_fd_copy = os.dup(_get_stderr_fileno()) + config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w") + faulthandler.enable(file=config.fault_handler_stderr) + + +def _get_stderr_fileno(): + try: + return sys.stderr.fileno() + except (AttributeError, io.UnsupportedOperation): + # python-xdist monkeypatches sys.stderr with an object that is not an actual file. + # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors + # This is potentially dangerous, but the best we can do. + return sys.__stderr__.fileno() + + +def pytest_unconfigure(config): + if config.getoption("fault_handler"): + import faulthandler + + faulthandler.disable() + # close our dup file installed during pytest_configure + f = getattr(config, "fault_handler_stderr", None) + if f is not None: + # re-enable the faulthandler, attaching it to the default sys.stderr + # so we can see crashes after pytest has finished, usually during + # garbage collection during interpreter shutdown + config.fault_handler_stderr.close() + del config.fault_handler_stderr + faulthandler.enable(file=_get_stderr_fileno()) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_protocol(item): + enabled = item.config.getoption("fault_handler") + timeout = item.config.getoption("fault_handler_timeout") + if enabled and timeout > 0: + import faulthandler + + stderr = item.config.fault_handler_stderr + faulthandler.dump_traceback_later(timeout, file=stderr) + try: + yield + finally: + faulthandler.cancel_dump_traceback_later() + else: + yield + + +@pytest.hookimpl(tryfirst=True) +def pytest_enter_pdb(): + """Cancel any traceback dumping due to timeout before entering pdb. + """ + import faulthandler + + faulthandler.cancel_dump_traceback_later() + + +@pytest.hookimpl(tryfirst=True) +def pytest_exception_interact(): + """Cancel any traceback dumping due to an interactive exception being + raised. + """ + import faulthandler + + faulthandler.cancel_dump_traceback_later() diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 177594c4a55..5cbb694b1d9 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,6 +1,7 @@ import os import pytest +from _pytest import deprecated from _pytest.warning_types import PytestDeprecationWarning from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG @@ -69,22 +70,14 @@ def test_terminal_reporter_writer_attr(pytestconfig): assert terminal_reporter.writer is terminal_reporter._tw -@pytest.mark.parametrize("plugin", ["catchlog", "capturelog"]) +@pytest.mark.parametrize("plugin", deprecated.DEPRECATED_EXTERNAL_PLUGINS) @pytest.mark.filterwarnings("default") -def test_pytest_catchlog_deprecated(testdir, plugin): - testdir.makepyfile( - """ - def test_func(pytestconfig): - pytestconfig.pluginmanager.register(None, 'pytest_{}') - """.format( - plugin - ) - ) - res = testdir.runpytest() - assert res.ret == 0 - res.stdout.fnmatch_lines( - ["*pytest-*log plugin has been merged into the core*", "*1 passed, 1 warnings*"] - ) +def test_external_plugins_integrated(testdir, plugin): + testdir.syspathinsert() + testdir.makepyfile(**{plugin: ""}) + + with pytest.warns(pytest.PytestConfigWarning): + testdir.parseconfig("-p", plugin) def test_raises_message_argument_deprecated(): diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py new file mode 100644 index 00000000000..d1f2e8b9a0e --- /dev/null +++ b/testing/test_faulthandler.py @@ -0,0 +1,99 @@ +import sys + +import pytest + + +def test_enabled(testdir): + """Test single crashing test displays a traceback.""" + testdir.makepyfile( + """ + import faulthandler + def test_crash(): + faulthandler._sigabrt() + """ + ) + result = testdir.runpytest_subprocess() + result.stderr.fnmatch_lines(["*Fatal Python error*"]) + assert result.ret != 0 + + +def test_crash_near_exit(testdir): + """Test that fault handler displays crashes that happen even after + pytest is exiting (for example, when the interpreter is shutting down). + """ + testdir.makepyfile( + """ + import faulthandler + import atexit + def test_ok(): + atexit.register(faulthandler._sigabrt) + """ + ) + result = testdir.runpytest_subprocess() + result.stderr.fnmatch_lines(["*Fatal Python error*"]) + assert result.ret != 0 + + +def test_disabled(testdir): + """Test option to disable fault handler in the command line. + """ + testdir.makepyfile( + """ + import faulthandler + def test_disabled(): + assert not faulthandler.is_enabled() + """ + ) + result = testdir.runpytest_subprocess("--no-faulthandler") + result.stdout.fnmatch_lines(["*1 passed*"]) + assert result.ret == 0 + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_timeout(testdir, enabled): + """Test option to dump tracebacks after a certain timeout. + If faulthandler is disabled, no traceback will be dumped. + """ + testdir.makepyfile( + """ + import time + def test_timeout(): + time.sleep(2.0) + """ + ) + args = ["--faulthandler-timeout=1"] + if not enabled: + args.append("--no-faulthandler") + + result = testdir.runpytest_subprocess(*args) + tb_output = "most recent call first" + if sys.version_info[:2] == (3, 3): + tb_output = "Thread" + if enabled: + result.stderr.fnmatch_lines(["*%s*" % tb_output]) + else: + assert tb_output not in result.stderr.str() + result.stdout.fnmatch_lines(["*1 passed*"]) + assert result.ret == 0 + + +@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"]) +def test_cancel_timeout_on_hook(monkeypatch, pytestconfig, hook_name): + """Make sure that we are cancelling any scheduled traceback dumping due + to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive + exception (pytest-dev/pytest-faulthandler#14). + """ + import faulthandler + from _pytest import faulthandler as plugin_module + + called = [] + + monkeypatch.setattr( + faulthandler, "cancel_dump_traceback_later", lambda: called.append(1) + ) + + # call our hook explicitly, we can trust that pytest will call the hook + # for us at the appropriate moment + hook_func = getattr(plugin_module, hook_name) + hook_func() + assert called == [1] From 3ce31b6370fcaa02a63f09c86a2859800d15984a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 22 Jun 2019 19:22:43 -0300 Subject: [PATCH 056/109] Change pytest-faulthandler for simplification * The --no-faulthandler option is not necessary given that we can use `-p no:faulthandler`. * The `--faulthandler-timeout` command-line option has become an ini option, for the reasons described in https://github.com/pytest-dev/pytest-faulthandler/issues/34 and users can still set it from the command-line. Fix pytest-dev/pytest-faulthandler#34 --- doc/en/reference.rst | 17 ++++++++++ doc/en/usage.rst | 20 +++++++++-- src/_pytest/faulthandler.py | 64 ++++++++++++++---------------------- testing/test_faulthandler.py | 12 ++++--- 4 files changed, 66 insertions(+), 47 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 0b168eb5442..7ad69d4a83e 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1076,6 +1076,23 @@ passed multiple times. The expected format is ``name=value``. For example:: for more details. +.. confval:: faulthandler_timeout + + Dumps the tracebacks of all threads if a test takes longer than ``X`` seconds to run (including + fixture setup and teardown). Implemented using the `faulthandler.dump_traceback_later`_ function, + so all caveats there apply. + + .. code-block:: ini + + # content of pytest.ini + [pytest] + faulthandler_timeout=5 + + For more information please refer to :ref:`faulthandler`. + +.. _`faulthandler.dump_traceback_later`: https://docs.python.org/3/library/faulthandler.html#faulthandler.dump_traceback_later + + .. confval:: filterwarnings diff --git a/doc/en/usage.rst b/doc/en/usage.rst index c1332706fc7..a8acc355106 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -428,11 +428,25 @@ Fault Handler The `faulthandler `__ standard module can be used to dump Python tracebacks on a segfault or after a timeout. -The module is automatically enabled for pytest runs, unless the ``--no-faulthandler`` is given +The module is automatically enabled for pytest runs, unless the ``-p no:faulthandler`` is given on the command-line. -Also the ``--faulthandler-timeout=X`` can be used to dump the traceback of all threads if a test -takes longer than ``X`` seconds to finish (not available on Windows). +Also the :confval:`faulthandler_timeout=X` configuration option can be used +to dump the traceback of all threads if a test takes longer than ``X`` +seconds to finish (not available on Windows). + +.. note:: + + This functionality has been integrated from the external + `pytest-faulthandler `__ plugin, with two + small differences: + + * To disable it, use ``-p no:faulthandler`` instead of ``--no-faulthandler``: the former + can be used with any plugin, so it saves one option. + + * The ``--faulthandler-timeout`` command-line option has become the + :confval:`faulthandler_timeout` configuration option. It can still be configured from + the command-line using ``-o faulthandler_timeout=X``. Creating JUnitXML format files diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 48fe0f218d5..068bec528dd 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -6,38 +6,24 @@ def pytest_addoption(parser): - group = parser.getgroup("terminal reporting") - group.addoption( - "--no-faulthandler", - action="store_false", - dest="fault_handler", - default=True, - help="Disable faulthandler module.", - ) - - group.addoption( - "--faulthandler-timeout", - type=float, - dest="fault_handler_timeout", - metavar="TIMEOUT", - default=0.0, - help="Dump the traceback of all threads if a test takes " + help = ( + "Dump the traceback of all threads if a test takes " "more than TIMEOUT seconds to finish.\n" - "Not available on Windows.", + "Not available on Windows." ) + parser.addini("faulthandler_timeout", help, default=0.0) def pytest_configure(config): - if config.getoption("fault_handler"): - import faulthandler + import faulthandler - # avoid trying to dup sys.stderr if faulthandler is already enabled - if faulthandler.is_enabled(): - return + # avoid trying to dup sys.stderr if faulthandler is already enabled + if faulthandler.is_enabled(): + return - stderr_fd_copy = os.dup(_get_stderr_fileno()) - config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w") - faulthandler.enable(file=config.fault_handler_stderr) + stderr_fd_copy = os.dup(_get_stderr_fileno()) + config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w") + faulthandler.enable(file=config.fault_handler_stderr) def _get_stderr_fileno(): @@ -51,26 +37,24 @@ def _get_stderr_fileno(): def pytest_unconfigure(config): - if config.getoption("fault_handler"): - import faulthandler + import faulthandler - faulthandler.disable() - # close our dup file installed during pytest_configure - f = getattr(config, "fault_handler_stderr", None) - if f is not None: - # re-enable the faulthandler, attaching it to the default sys.stderr - # so we can see crashes after pytest has finished, usually during - # garbage collection during interpreter shutdown - config.fault_handler_stderr.close() - del config.fault_handler_stderr - faulthandler.enable(file=_get_stderr_fileno()) + faulthandler.disable() + # close our dup file installed during pytest_configure + f = getattr(config, "fault_handler_stderr", None) + if f is not None: + # re-enable the faulthandler, attaching it to the default sys.stderr + # so we can see crashes after pytest has finished, usually during + # garbage collection during interpreter shutdown + config.fault_handler_stderr.close() + del config.fault_handler_stderr + faulthandler.enable(file=_get_stderr_fileno()) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_protocol(item): - enabled = item.config.getoption("fault_handler") - timeout = item.config.getoption("fault_handler_timeout") - if enabled and timeout > 0: + timeout = float(item.config.getini("faulthandler_timeout") or 0.0) + if timeout > 0: import faulthandler stderr = item.config.fault_handler_stderr diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index d1f2e8b9a0e..a0cf1d8c128 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -44,7 +44,7 @@ def test_disabled(): assert not faulthandler.is_enabled() """ ) - result = testdir.runpytest_subprocess("--no-faulthandler") + result = testdir.runpytest_subprocess("-p", "no:faulthandler") result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == 0 @@ -61,9 +61,13 @@ def test_timeout(): time.sleep(2.0) """ ) - args = ["--faulthandler-timeout=1"] - if not enabled: - args.append("--no-faulthandler") + testdir.makeini( + """ + [pytest] + faulthandler_timeout = 1 + """ + ) + args = ["-p", "no:faulthandler"] if not enabled else [] result = testdir.runpytest_subprocess(*args) tb_output = "most recent call first" From 01a094cc4316a2840254a66f829367f3d67fd442 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 24 Jun 2019 06:05:24 +0200 Subject: [PATCH 057/109] minor: clarify help with reportchars `-ra` / `-rA` will include "w" also. This does not explicitly mention it (allowing for change the behavior), but makes it a) clearer that "w" is a recognized reportchar, and b) less confusing that `-ra --disable-warnings` still displays them. --- src/_pytest/terminal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 91e37385276..bcd6e1f7c95 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -76,8 +76,7 @@ def pytest_addoption(parser): help="show extra test summary info as specified by chars: (f)ailed, " "(E)rror, (s)kipped, (x)failed, (X)passed, " "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. " - "Warnings are displayed at all times except when " - "--disable-warnings is set.", + "(w)arnings are enabled by default (see --disable-warnings).", ) group._addoption( "--disable-warnings", From 9a89783fbb8e17984cf7c9ab8cb83afcb8a9f715 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Mon, 24 Jun 2019 16:09:39 +0200 Subject: [PATCH 058/109] Assertion passed hook --- changelog/3457.feature.rst | 2 + setup.py | 1 + src/_pytest/assertion/__init__.py | 8 ++- src/_pytest/assertion/rewrite.py | 89 +++++++++++++++++++++-------- src/_pytest/assertion/util.py | 4 ++ src/_pytest/hookspec.py | 15 +++++ testing/acceptance_test.py | 45 --------------- testing/fixture_values_leak_test.py | 53 +++++++++++++++++ testing/python/raises.py | 3 + testing/test_assertrewrite.py | 51 +++++++++++++++++ 10 files changed, 202 insertions(+), 69 deletions(-) create mode 100644 changelog/3457.feature.rst create mode 100644 testing/fixture_values_leak_test.py diff --git a/changelog/3457.feature.rst b/changelog/3457.feature.rst new file mode 100644 index 00000000000..3f676514431 --- /dev/null +++ b/changelog/3457.feature.rst @@ -0,0 +1,2 @@ +Adds ``pytest_assertion_pass`` hook, called with assertion context information +(original asssertion statement and pytest explanation) whenever an assertion passes. diff --git a/setup.py b/setup.py index 4c87c6429bb..7d953281611 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ "pluggy>=0.12,<1.0", "importlib-metadata>=0.12", "wcwidth", + "astor", ] diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index e52101c9f16..b59b1bfdffc 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -92,7 +92,7 @@ def pytest_collection(session): def pytest_runtest_setup(item): - """Setup the pytest_assertrepr_compare hook + """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks The newinterpret and rewrite modules will use util._reprcompare if it exists to use custom reporting via the @@ -129,9 +129,15 @@ def callbinrepr(op, left, right): util._reprcompare = callbinrepr + if item.ihook.pytest_assertion_pass.get_hookimpls(): + def call_assertion_pass_hook(lineno, expl, orig): + item.ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl) + util._assertion_pass = call_assertion_pass_hook + def pytest_runtest_teardown(item): util._reprcompare = None + util._assertion_pass = None def pytest_sessionfinish(session): diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ce698f368f2..77dab5242e9 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1,5 +1,6 @@ """Rewrite assertion AST to produce nice error messages""" import ast +import astor import errno import imp import itertools @@ -357,6 +358,11 @@ def _rewrite_test(config, fn): state.trace("failed to parse: {!r}".format(fn)) return None, None rewrite_asserts(tree, fn, config) + + # TODO: REMOVE, THIS IS ONLY FOR DEBUG + with open(f'{str(fn)+"bak"}', "w", encoding="utf-8") as f: + f.write(astor.to_source(tree)) + try: co = compile(tree, fn.strpath, "exec", dont_inherit=True) except SyntaxError: @@ -434,7 +440,7 @@ def _format_assertmsg(obj): # contains a newline it gets escaped, however if an object has a # .__repr__() which contains newlines it does not get escaped. # However in either case we want to preserve the newline. - replaces = [("\n", "\n~"), ("%", "%%")] + replaces = [("\n", "\n~")] if not isinstance(obj, str): obj = saferepr(obj) replaces.append(("\\n", "\n~")) @@ -478,6 +484,17 @@ def _call_reprcompare(ops, results, expls, each_obj): return expl +def _call_assertion_pass(lineno, orig, expl): + if util._assertion_pass is not None: + util._assertion_pass(lineno=lineno, orig=orig, expl=expl) + + +def _check_if_assertionpass_impl(): + """Checks if any plugins implement the pytest_assertion_pass hook + in order not to generate explanation unecessarily (might be expensive)""" + return True if util._assertion_pass else False + + unary_map = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"} binop_map = { @@ -550,7 +567,8 @@ class AssertionRewriter(ast.NodeVisitor): original assert statement: it rewrites the test of an assertion to provide intermediate values and replace it with an if statement which raises an assertion error with a detailed explanation in - case the expression is false. + case the expression is false and calls pytest_assertion_pass hook + if expression is true. For this .visit_Assert() uses the visitor pattern to visit all the AST nodes of the ast.Assert.test field, each visit call returning @@ -568,9 +586,10 @@ class AssertionRewriter(ast.NodeVisitor): by statements. Variables are created using .variable() and have the form of "@py_assert0". - :on_failure: The AST statements which will be executed if the - assertion test fails. This is the code which will construct - the failure message and raises the AssertionError. + :expl_stmts: The AST statements which will be executed to get + data from the assertion. This is the code which will construct + the detailed assertion message that is used in the AssertionError + or for the pytest_assertion_pass hook. :explanation_specifiers: A dict filled by .explanation_param() with %-formatting placeholders and their corresponding @@ -720,7 +739,7 @@ def pop_format_context(self, expl_expr): The expl_expr should be an ast.Str instance constructed from the %-placeholders created by .explanation_param(). This will - add the required code to format said string to .on_failure and + add the required code to format said string to .expl_stmts and return the ast.Name instance of the formatted string. """ @@ -731,7 +750,8 @@ def pop_format_context(self, expl_expr): format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) - self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) + self.format_variables.append(name) + self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) return ast.Name(name, ast.Load()) def generic_visit(self, node): @@ -765,8 +785,9 @@ def visit_Assert(self, assert_): self.statements = [] self.variables = [] self.variable_counter = itertools.count() + self.format_variables = [] self.stack = [] - self.on_failure = [] + self.expl_stmts = [] self.push_format_context() # Rewrite assert into a bunch of statements. top_condition, explanation = self.visit(assert_.test) @@ -777,24 +798,46 @@ def visit_Assert(self, assert_): top_condition, module_path=self.module_path, lineno=assert_.lineno ) ) - # Create failure message. - body = self.on_failure negation = ast.UnaryOp(ast.Not(), top_condition) - self.statements.append(ast.If(negation, body, [])) + msg = self.pop_format_context(ast.Str(explanation)) if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) - explanation = "\n>assert " + explanation + gluestr = "\n>assert " else: assertmsg = ast.Str("") - explanation = "assert " + explanation - template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) - msg = self.pop_format_context(template) - fmt = self.helper("_format_explanation", msg) + gluestr = "assert " + err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) + err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_name = ast.Name("AssertionError", ast.Load()) + fmt = self.helper("_format_explanation", err_msg) + fmt_pass = self.helper("_format_explanation", msg) exc = ast.Call(err_name, [fmt], []) - raise_ = ast.Raise(exc, None) + if sys.version_info[0] >= 3: + raise_ = ast.Raise(exc, None) + else: + raise_ = ast.Raise(exc, None, None) + # Call to hook when passes + orig = astor.to_source(assert_.test).rstrip("\n").lstrip("(").rstrip(")") + hook_call_pass = ast.Expr( + self.helper( + "_call_assertion_pass", ast.Num(assert_.lineno), ast.Str(orig), fmt_pass + ) + ) - body.append(raise_) + # If any hooks implement assert_pass hook + hook_impl_test = ast.If( + self.helper("_check_if_assertionpass_impl"), + [hook_call_pass], + [], + ) + main_test = ast.If(negation, [raise_], [hook_impl_test]) + + self.statements.extend(self.expl_stmts) + self.statements.append(main_test) + if self.format_variables: + variables = [ast.Name(name, ast.Store()) for name in self.format_variables] + clear_format = ast.Assign(variables, _NameConstant(None)) + self.statements.append(clear_format) # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] @@ -848,7 +891,7 @@ def visit_BoolOp(self, boolop): app = ast.Attribute(expl_list, "append", ast.Load()) is_or = int(isinstance(boolop.op, ast.Or)) body = save = self.statements - fail_save = self.on_failure + fail_save = self.expl_stmts levels = len(boolop.values) - 1 self.push_format_context() # Process each operand, short-circuting if needed. @@ -856,14 +899,14 @@ def visit_BoolOp(self, boolop): if i: fail_inner = [] # cond is set in a prior loop iteration below - self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa - self.on_failure = fail_inner + self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa + self.expl_stmts = fail_inner self.push_format_context() 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], []) - self.on_failure.append(ast.Expr(call)) + self.expl_stmts.append(ast.Expr(call)) if i < levels: cond = res if is_or: @@ -872,7 +915,7 @@ def visit_BoolOp(self, boolop): self.statements.append(ast.If(cond, inner, [])) self.statements = body = inner self.statements = save - self.on_failure = fail_save + self.expl_stmts = fail_save expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 762e5761d71..df4587985fe 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -12,6 +12,10 @@ # DebugInterpreter. _reprcompare = None +# Works similarly as _reprcompare attribute. Is populated with the hook call +# when pytest_runtest_setup is called. +_assertion_pass = None + def format_explanation(explanation): """This formats an explanation diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index d40a368116c..70301566caa 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -485,6 +485,21 @@ def pytest_assertrepr_compare(config, op, left, right): """ +def pytest_assertion_pass(item, lineno, orig, expl): + """Process explanation when assertions are valid. + + Use this hook to do some processing after a passing assertion. + The original assertion information is available in the `orig` string + and the pytest introspected assertion information is available in the + `expl` string. + + :param _pytest.nodes.Item item: pytest item object of current test + :param int lineno: line number of the assert statement + :param string orig: string with original assertion + :param string expl: string with assert explanation + """ + + # ------------------------------------------------------------------------- # hooks for influencing reporting (invoked from _pytest_terminal) # ------------------------------------------------------------------------- diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 60cc21c4a01..ccb69dd79d1 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1047,51 +1047,6 @@ def test(request): result.stdout.fnmatch_lines(["* 1 passed *"]) -def test_fixture_values_leak(testdir): - """Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected - life-times (#2981). - """ - testdir.makepyfile( - """ - import attr - import gc - import pytest - import weakref - - @attr.s - class SomeObj(object): - name = attr.ib() - - fix_of_test1_ref = None - session_ref = None - - @pytest.fixture(scope='session') - def session_fix(): - global session_ref - obj = SomeObj(name='session-fixture') - session_ref = weakref.ref(obj) - return obj - - @pytest.fixture - def fix(session_fix): - global fix_of_test1_ref - obj = SomeObj(name='local-fixture') - fix_of_test1_ref = weakref.ref(obj) - return obj - - def test1(fix): - assert fix_of_test1_ref() is fix - - def test2(): - gc.collect() - # fixture "fix" created during test1 must have been destroyed by now - assert fix_of_test1_ref() is None - """ - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["* 2 passed *"]) - - def test_fixture_order_respects_scope(testdir): """Ensure that fixtures are created according to scope order, regression test for #2405 """ diff --git a/testing/fixture_values_leak_test.py b/testing/fixture_values_leak_test.py new file mode 100644 index 00000000000..6f4c90d3e07 --- /dev/null +++ b/testing/fixture_values_leak_test.py @@ -0,0 +1,53 @@ +"""Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected +life-times (#2981). + +This comes from the old acceptance_test.py::test_fixture_values_leak(testdir): +This used pytester before but was not working when using pytest_assert_reprcompare +because pytester tracks hook calls and it would hold a reference (ParsedCall object), +preventing garbage collection + +, + 'op': 'is', + 'left': SomeObj(name='local-fixture'), + 'right': SomeObj(name='local-fixture')})> +""" +import attr +import gc +import pytest +import weakref + + +@attr.s +class SomeObj(object): + name = attr.ib() + + +fix_of_test1_ref = None +session_ref = None + + +@pytest.fixture(scope="session") +def session_fix(): + global session_ref + obj = SomeObj(name="session-fixture") + session_ref = weakref.ref(obj) + return obj + + +@pytest.fixture +def fix(session_fix): + global fix_of_test1_ref + obj = SomeObj(name="local-fixture") + fix_of_test1_ref = weakref.ref(obj) + return obj + + +def test1(fix): + assert fix_of_test1_ref() is fix + + +def test2(): + gc.collect() + # fixture "fix" created during test1 must have been destroyed by now + assert fix_of_test1_ref() is None diff --git a/testing/python/raises.py b/testing/python/raises.py index 89cef38f1a2..c9ede412ae9 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -202,6 +202,9 @@ def __call__(self): assert sys.exc_info() == (None, None, None) del t + # Make sure this does get updated in locals dict + # otherwise it could keep a reference + locals() # ensure the t instance is not stuck in a cyclic reference for o in gc.get_objects(): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 0e6f42239f2..8304cf05771 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1305,3 +1305,54 @@ def test(): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["* 1 passed in *"]) + + +class TestAssertionPass: + def test_hook_call(self, testdir): + testdir.makeconftest( + """ + def pytest_assertion_pass(item, lineno, orig, expl): + raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno)) + """ + ) + testdir.makepyfile( + """ + def test_simple(): + a=1 + b=2 + c=3 + d=0 + + assert a+b == c+d + """ + ) + result = testdir.runpytest() + print(testdir.tmpdir) + result.stdout.fnmatch_lines( + "*Assertion Passed: a + b == c + d (1 + 2) == (3 + 0) at line 7*" + ) + + def test_hook_not_called_without_hookimpl(self, testdir, monkeypatch): + """Assertion pass should not be called (and hence formatting should + not occur) if there is no hook declared for pytest_assertion_pass""" + + def raise_on_assertionpass(*_, **__): + raise Exception("Assertion passed called when it shouldn't!") + + monkeypatch.setattr(_pytest.assertion.rewrite, + "_call_assertion_pass", + raise_on_assertionpass) + + testdir.makepyfile( + """ + def test_simple(): + a=1 + b=2 + c=3 + d=0 + + assert a+b == c+d + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) From 52e695b3295cbef9a243fbcb9d80b69e805dfbe1 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Mon, 24 Jun 2019 17:47:48 +0200 Subject: [PATCH 059/109] Removed debug code. --- src/_pytest/assertion/rewrite.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 77dab5242e9..528c3500bb4 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -358,11 +358,6 @@ def _rewrite_test(config, fn): state.trace("failed to parse: {!r}".format(fn)) return None, None rewrite_asserts(tree, fn, config) - - # TODO: REMOVE, THIS IS ONLY FOR DEBUG - with open(f'{str(fn)+"bak"}', "w", encoding="utf-8") as f: - f.write(astor.to_source(tree)) - try: co = compile(tree, fn.strpath, "exec", dont_inherit=True) except SyntaxError: From 4cd08f9b52e3ed984cf68b58abb6f7350623231f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 22 Jun 2019 15:02:32 -0700 Subject: [PATCH 060/109] Switch from deprecated imp to importlib --- changelog/1403.bugfix.rst | 1 + changelog/2761.bugfix.rst | 1 + changelog/5078.bugfix.rst | 1 + changelog/5432.bugfix.rst | 1 + changelog/5433.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 237 ++++++++++--------------------- src/_pytest/main.py | 22 ++- src/_pytest/pathlib.py | 2 + src/_pytest/pytester.py | 19 +-- testing/acceptance_test.py | 13 ++ testing/test_assertion.py | 4 +- testing/test_assertrewrite.py | 115 ++++++++------- testing/test_pathlib.py | 5 + 13 files changed, 181 insertions(+), 241 deletions(-) create mode 100644 changelog/1403.bugfix.rst create mode 100644 changelog/2761.bugfix.rst create mode 100644 changelog/5078.bugfix.rst create mode 100644 changelog/5432.bugfix.rst create mode 100644 changelog/5433.bugfix.rst diff --git a/changelog/1403.bugfix.rst b/changelog/1403.bugfix.rst new file mode 100644 index 00000000000..3fb748aec59 --- /dev/null +++ b/changelog/1403.bugfix.rst @@ -0,0 +1 @@ +Switch from ``imp`` to ``importlib``. diff --git a/changelog/2761.bugfix.rst b/changelog/2761.bugfix.rst new file mode 100644 index 00000000000..c63f02ecd58 --- /dev/null +++ b/changelog/2761.bugfix.rst @@ -0,0 +1 @@ +Honor PEP 235 on case-insensitive file systems. diff --git a/changelog/5078.bugfix.rst b/changelog/5078.bugfix.rst new file mode 100644 index 00000000000..8fed85f5da9 --- /dev/null +++ b/changelog/5078.bugfix.rst @@ -0,0 +1 @@ +Test module is no longer double-imported when using ``--pyargs``. diff --git a/changelog/5432.bugfix.rst b/changelog/5432.bugfix.rst new file mode 100644 index 00000000000..44c01c0cf59 --- /dev/null +++ b/changelog/5432.bugfix.rst @@ -0,0 +1 @@ +Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times. diff --git a/changelog/5433.bugfix.rst b/changelog/5433.bugfix.rst new file mode 100644 index 00000000000..c3a7472bc6c --- /dev/null +++ b/changelog/5433.bugfix.rst @@ -0,0 +1 @@ +Fix assertion rewriting in packages (``__init__.py``). diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ce698f368f2..cce98000530 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1,18 +1,16 @@ """Rewrite assertion AST to produce nice error messages""" import ast import errno -import imp +import importlib.machinery +import importlib.util import itertools import marshal import os -import re import struct import sys import types -from importlib.util import spec_from_file_location import atomicwrites -import py from _pytest._io.saferepr import saferepr from _pytest.assertion import util @@ -23,23 +21,13 @@ from _pytest.pathlib import PurePath # pytest caches rewritten pycs in __pycache__. -if hasattr(imp, "get_tag"): - PYTEST_TAG = imp.get_tag() + "-PYTEST" -else: - if hasattr(sys, "pypy_version_info"): - impl = "pypy" - else: - impl = "cpython" - ver = sys.version_info - PYTEST_TAG = "{}-{}{}-PYTEST".format(impl, ver[0], ver[1]) - del ver, impl - +PYTEST_TAG = "{}-PYTEST".format(sys.implementation.cache_tag) PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT class AssertionRewritingHook: - """PEP302 Import hook which rewrites asserts.""" + """PEP302/PEP451 import hook which rewrites asserts.""" def __init__(self, config): self.config = config @@ -48,7 +36,6 @@ def __init__(self, config): except ValueError: self.fnpats = ["test_*.py", "*_test.py"] self.session = None - self.modules = {} self._rewritten_names = set() self._must_rewrite = set() # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, @@ -62,55 +49,51 @@ def set_session(self, session): self.session = session self._session_paths_checked = False - def _imp_find_module(self, name, path=None): - """Indirection so we can mock calls to find_module originated from the hook during testing""" - return imp.find_module(name, path) + # Indirection so we can mock calls to find_spec originated from the hook during testing + _find_spec = importlib.machinery.PathFinder.find_spec - def find_module(self, name, path=None): + def find_spec(self, name, path=None, target=None): if self._writing_pyc: return None state = self.config._assertstate if self._early_rewrite_bailout(name, state): return None state.trace("find_module called for: %s" % name) - names = name.rsplit(".", 1) - lastname = names[-1] - pth = None - if path is not None: - # Starting with Python 3.3, path is a _NamespacePath(), which - # causes problems if not converted to list. - path = list(path) - if len(path) == 1: - pth = path[0] - if pth is None: - try: - fd, fn, desc = self._imp_find_module(lastname, path) - except ImportError: - return None - if fd is not None: - fd.close() - tp = desc[2] - if tp == imp.PY_COMPILED: - if hasattr(imp, "source_from_cache"): - 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: - # Don't know what this is. - return None + + spec = self._find_spec(name, path) + if ( + # the import machinery could not find a file to import + spec is None + # this is a namespace package (without `__init__.py`) + # there's nothing to rewrite there + # python3.5 - python3.6: `namespace` + # python3.7+: `None` + or spec.origin in {None, "namespace"} + # if the file doesn't exist, we can't rewrite it + or not os.path.exists(spec.origin) + ): + return None else: - fn = os.path.join(pth, name.rpartition(".")[2] + ".py") + fn = spec.origin - fn_pypath = py.path.local(fn) - if not self._should_rewrite(name, fn_pypath, state): + if not self._should_rewrite(name, fn, state): return None - self._rewritten_names.add(name) + return importlib.util.spec_from_file_location( + name, + fn, + loader=self, + submodule_search_locations=spec.submodule_search_locations, + ) + + def create_module(self, spec): + return None # default behaviour is fine + + def exec_module(self, module): + fn = module.__spec__.origin + state = self.config._assertstate + + self._rewritten_names.add(module.__name__) # The requested module looks like a test file, so rewrite it. This is # the most magical part of the process: load the source, rewrite the @@ -121,7 +104,7 @@ def find_module(self, name, path=None): # cached pyc is always a complete, valid pyc. Operations on it must be # atomic. POSIX's atomic rename comes in handy. write = not sys.dont_write_bytecode - cache_dir = os.path.join(fn_pypath.dirname, "__pycache__") + cache_dir = os.path.join(os.path.dirname(fn), "__pycache__") if write: try: os.mkdir(cache_dir) @@ -132,26 +115,23 @@ def find_module(self, name, path=None): # common case) or it's blocked by a non-dir node. In the # latter case, we'll ignore it in _write_pyc. pass - elif e in [errno.ENOENT, errno.ENOTDIR]: + elif e in {errno.ENOENT, errno.ENOTDIR}: # One of the path components was not a directory, likely # because we're in a zip file. write = False - elif e in [errno.EACCES, errno.EROFS, errno.EPERM]: - state.trace("read only directory: %r" % fn_pypath.dirname) + elif e in {errno.EACCES, errno.EROFS, errno.EPERM}: + state.trace("read only directory: %r" % os.path.dirname(fn)) write = False else: raise - cache_name = fn_pypath.basename[:-3] + PYC_TAIL + cache_name = os.path.basename(fn)[:-3] + PYC_TAIL pyc = os.path.join(cache_dir, cache_name) # Notice that even if we're in a read-only directory, I'm going # to check for a cached pyc. This may not be optimal... - co = _read_pyc(fn_pypath, pyc, state.trace) + co = _read_pyc(fn, pyc, state.trace) if co is None: state.trace("rewriting {!r}".format(fn)) - source_stat, co = _rewrite_test(self.config, fn_pypath) - if co is None: - # Probably a SyntaxError in the test. - return None + source_stat, co = _rewrite_test(fn) if write: self._writing_pyc = True try: @@ -160,13 +140,11 @@ def find_module(self, name, path=None): self._writing_pyc = False else: state.trace("found cached rewritten pyc for {!r}".format(fn)) - self.modules[name] = co, pyc - return self + exec(co, module.__dict__) def _early_rewrite_bailout(self, name, state): - """ - This is a fast way to get out of rewriting modules. Profiling has - shown that the call to imp.find_module (inside of the find_module + """This is a fast way to get out of rewriting modules. Profiling has + shown that the call to PathFinder.find_spec (inside of the find_spec from this class) is a major slowdown, so, this method tries to filter what we're sure won't be rewritten before getting to it. """ @@ -201,10 +179,9 @@ def _early_rewrite_bailout(self, name, state): state.trace("early skip of rewriting module: {}".format(name)) return True - def _should_rewrite(self, name, fn_pypath, state): + def _should_rewrite(self, name, fn, state): # always rewrite conftest files - fn = str(fn_pypath) - if fn_pypath.basename == "conftest.py": + if os.path.basename(fn) == "conftest.py": state.trace("rewriting conftest file: {!r}".format(fn)) return True @@ -217,8 +194,9 @@ def _should_rewrite(self, name, fn_pypath, state): # modules not passed explicitly on the command line are only # rewritten if they match the naming convention for test files + fn_path = PurePath(fn) for pat in self.fnpats: - if fn_pypath.fnmatch(pat): + if fnmatch_ex(pat, fn_path): state.trace("matched test file {!r}".format(fn)) return True @@ -249,9 +227,10 @@ def mark_rewrite(self, *names): set(names).intersection(sys.modules).difference(self._rewritten_names) ) for name in already_imported: + mod = sys.modules[name] if not AssertionRewriter.is_rewrite_disabled( - sys.modules[name].__doc__ or "" - ): + mod.__doc__ or "" + ) and not isinstance(mod.__loader__, type(self)): self._warn_already_imported(name) self._must_rewrite.update(names) self._marked_for_rewrite_cache.clear() @@ -268,45 +247,8 @@ def _warn_already_imported(self, name): stacklevel=5, ) - def load_module(self, name): - co, pyc = self.modules.pop(name) - if name in sys.modules: - # If there is an existing module object named 'fullname' in - # sys.modules, the loader must use that existing module. (Otherwise, - # the reload() builtin will not work correctly.) - mod = sys.modules[name] - else: - # I wish I could just call imp.load_compiled here, but __file__ has to - # be set properly. In Python 3.2+, this all would be handled correctly - # by load_compiled. - mod = sys.modules[name] = imp.new_module(name) - try: - mod.__file__ = co.co_filename - # Normally, this attribute is 3.2+. - mod.__cached__ = pyc - mod.__loader__ = self - # Normally, this attribute is 3.4+ - mod.__spec__ = spec_from_file_location(name, co.co_filename, loader=self) - exec(co, mod.__dict__) - except: # noqa - if name in sys.modules: - del sys.modules[name] - raise - return sys.modules[name] - - def is_package(self, name): - try: - fd, fn, desc = self._imp_find_module(name) - except ImportError: - return False - if fd is not None: - fd.close() - tp = desc[2] - return tp == imp.PKG_DIRECTORY - def get_data(self, pathname): - """Optional PEP302 get_data API. - """ + """Optional PEP302 get_data API.""" with open(pathname, "rb") as f: return f.read() @@ -314,15 +256,13 @@ def get_data(self, pathname): def _write_pyc(state, co, source_stat, pyc): # Technically, we don't have to have the same pyc format as # (C)Python, since these "pycs" should never be seen by builtin - # import. However, there's little reason deviate, and I hope - # sometime to be able to use imp.load_compiled to load them. (See - # the comment in load_module above.) + # import. However, there's little reason deviate. try: with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp: - fp.write(imp.get_magic()) + fp.write(importlib.util.MAGIC_NUMBER) # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) - mtime = int(source_stat.mtime) & 0xFFFFFFFF - size = source_stat.size & 0xFFFFFFFF + mtime = int(source_stat.st_mtime) & 0xFFFFFFFF + size = source_stat.st_size & 0xFFFFFFFF # " strip_bytes pyc.write(contents[:strip_bytes], mode="wb") - assert _read_pyc(source, str(pyc)) is None # no error + assert _read_pyc(str(source), str(pyc)) is None # no error def test_reload_is_same(self, testdir): # A file that will be picked up during collecting. @@ -1186,14 +1192,17 @@ def spy_write_pyc(*args, **kwargs): # make a note that we have called _write_pyc write_pyc_called.append(True) # try to import a module at this point: we should not try to rewrite this module - assert hook.find_module("test_bar") is None + assert hook.find_spec("test_bar") is None return original_write_pyc(*args, **kwargs) monkeypatch.setattr(rewrite, "_write_pyc", spy_write_pyc) monkeypatch.setattr(sys, "dont_write_bytecode", False) hook = AssertionRewritingHook(pytestconfig) - assert hook.find_module("test_foo") is not None + spec = hook.find_spec("test_foo") + assert spec is not None + module = importlib.util.module_from_spec(spec) + hook.exec_module(module) assert len(write_pyc_called) == 1 @@ -1201,11 +1210,11 @@ class TestEarlyRewriteBailout: @pytest.fixture def hook(self, pytestconfig, monkeypatch, testdir): """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track - if imp.find_module has been called. + if PathFinder.find_spec has been called. """ - import imp + import importlib.machinery - self.find_module_calls = [] + self.find_spec_calls = [] self.initial_paths = set() class StubSession: @@ -1214,22 +1223,22 @@ class StubSession: def isinitpath(self, p): return p in self._initialpaths - def spy_imp_find_module(name, path): - self.find_module_calls.append(name) - return imp.find_module(name, path) + def spy_find_spec(name, path): + self.find_spec_calls.append(name) + return importlib.machinery.PathFinder.find_spec(name, path) hook = AssertionRewritingHook(pytestconfig) # use default patterns, otherwise we inherit pytest's testing config hook.fnpats[:] = ["test_*.py", "*_test.py"] - monkeypatch.setattr(hook, "_imp_find_module", spy_imp_find_module) + monkeypatch.setattr(hook, "_find_spec", spy_find_spec) hook.set_session(StubSession()) testdir.syspathinsert() return hook def test_basic(self, testdir, hook): """ - Ensure we avoid calling imp.find_module when we know for sure a certain module will not be rewritten - to optimize assertion rewriting (#3918). + Ensure we avoid calling PathFinder.find_spec when we know for sure a certain + module will not be rewritten to optimize assertion rewriting (#3918). """ testdir.makeconftest( """ @@ -1244,24 +1253,24 @@ def fix(): return 1 self.initial_paths.add(foobar_path) # conftest files should always be rewritten - assert hook.find_module("conftest") is not None - assert self.find_module_calls == ["conftest"] + assert hook.find_spec("conftest") is not None + assert self.find_spec_calls == ["conftest"] # files matching "python_files" mask should always be rewritten - assert hook.find_module("test_foo") is not None - assert self.find_module_calls == ["conftest", "test_foo"] + assert hook.find_spec("test_foo") is not None + assert self.find_spec_calls == ["conftest", "test_foo"] # file does not match "python_files": early bailout - assert hook.find_module("bar") is None - assert self.find_module_calls == ["conftest", "test_foo"] + assert hook.find_spec("bar") is None + assert self.find_spec_calls == ["conftest", "test_foo"] # file is an initial path (passed on the command-line): should be rewritten - assert hook.find_module("foobar") is not None - assert self.find_module_calls == ["conftest", "test_foo", "foobar"] + assert hook.find_spec("foobar") is not None + assert self.find_spec_calls == ["conftest", "test_foo", "foobar"] def test_pattern_contains_subdirectories(self, testdir, hook): """If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early - because we need to match with the full path, which can only be found by calling imp.find_module. + because we need to match with the full path, which can only be found by calling PathFinder.find_spec """ p = testdir.makepyfile( **{ @@ -1273,8 +1282,8 @@ def test_simple_failure(): ) testdir.syspathinsert(p.dirpath()) hook.fnpats[:] = ["tests/**.py"] - assert hook.find_module("file") is not None - assert self.find_module_calls == ["file"] + assert hook.find_spec("file") is not None + assert self.find_spec_calls == ["file"] @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 8ac4040702a..45daeaed76a 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,3 +1,4 @@ +import os.path import sys import py @@ -53,6 +54,10 @@ def match_(pattern, path): def test_matching(self, match, pattern, path): assert match(pattern, path) + def test_matching_abspath(self, match): + abspath = os.path.abspath(os.path.join("tests/foo.py")) + assert match("tests/foo.py", abspath) + @pytest.mark.parametrize( "pattern, path", [ From 380ca8f8805ee6bbdbb7c9de8a3c136c2614bab1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 24 Jun 2019 11:24:03 -0700 Subject: [PATCH 061/109] Use new raise syntax in one case --- src/_pytest/outcomes.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 749e80f3cb6..fb4d471b53c 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -149,7 +149,6 @@ def importorskip(modname, minversion=None, reason=None): __tracebackhide__ = True compile(modname, "", "eval") # to catch syntaxerrors - import_exc = None with warnings.catch_warnings(): # make sure to ignore ImportWarnings that might happen because @@ -159,12 +158,9 @@ def importorskip(modname, minversion=None, reason=None): try: __import__(modname) except ImportError as exc: - # Do not raise chained exception here(#1485) - import_exc = exc - if import_exc: - if reason is None: - reason = "could not import {!r}: {}".format(modname, import_exc) - raise Skipped(reason, allow_module_level=True) + if reason is None: + reason = "could not import {!r}: {}".format(modname, exc) + raise Skipped(reason, allow_module_level=True) from None mod = sys.modules[modname] if minversion is None: return mod From f43fb131797da038ba28917d1b7c775b4c93c5dd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 24 Jun 2019 20:37:07 -0300 Subject: [PATCH 062/109] Include pytest version in the cached pyc tags Fix #1671 --- changelog/1671.bugfix.rst | 2 ++ src/_pytest/assertion/rewrite.py | 3 ++- testing/test_assertrewrite.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 changelog/1671.bugfix.rst diff --git a/changelog/1671.bugfix.rst b/changelog/1671.bugfix.rst new file mode 100644 index 00000000000..c46eac82866 --- /dev/null +++ b/changelog/1671.bugfix.rst @@ -0,0 +1,2 @@ +The name of the ``.pyc`` files cached by the assertion writer now includes the pytest version +to avoid stale caches. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index cce98000530..f50d8200e89 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,6 +13,7 @@ import atomicwrites from _pytest._io.saferepr import saferepr +from _pytest._version import version from _pytest.assertion import util from _pytest.assertion.util import ( # noqa: F401 format_explanation as _format_explanation, @@ -21,7 +22,7 @@ from _pytest.pathlib import PurePath # pytest caches rewritten pycs in __pycache__. -PYTEST_TAG = "{}-PYTEST".format(sys.implementation.cache_tag) +PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version) PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 33c889104bd..258b5d3b788 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -780,6 +780,24 @@ def test_it(): assert testdir.runpytest().ret == 0 + def test_cached_pyc_includes_pytest_version(self, testdir, monkeypatch): + """Avoid stale caches (#1671)""" + monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) + testdir.makepyfile( + test_foo=""" + def test_foo(): + assert True + """ + ) + result = testdir.runpytest_subprocess() + assert result.ret == 0 + found_names = glob.glob( + "__pycache__/*-pytest-{}.pyc".format(pytest.__version__) + ) + assert found_names, "pyc with expected tag not found in names: {}".format( + glob.glob("__pycache__/*.pyc") + ) + @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): testdir.makepyfile( From 95714436a1ec29e01f017c914cee63970acdcb2a Mon Sep 17 00:00:00 2001 From: "Kevin J. Foley" Date: Mon, 24 Jun 2019 19:07:40 -0400 Subject: [PATCH 063/109] Pickup additional positional args passed to _parse_parametrize_args --- AUTHORS | 1 + changelog/5482.bugfix.rst | 2 ++ src/_pytest/mark/structures.py | 5 +---- testing/python/metafunc.py | 13 +++++++++++++ 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 changelog/5482.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 3d050a346dc..087fce8d0b7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -135,6 +135,7 @@ Kale Kundert Katarzyna Jachim Katerina Koukiou Kevin Cox +Kevin J. Foley Kodi B. Arfer Kostis Anagnostopoulos Kristoffer Nordström diff --git a/changelog/5482.bugfix.rst b/changelog/5482.bugfix.rst new file mode 100644 index 00000000000..c345458d18e --- /dev/null +++ b/changelog/5482.bugfix.rst @@ -0,0 +1,2 @@ +Fix bug introduced in 4.6.0 causing collection errors when passing +more than 2 positional arguments to ``pytest.mark.parametrize``. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 39cdb57e46d..1af7a9b42f2 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -102,10 +102,7 @@ def extract_from(cls, parameterset, force_tuple=False): return cls(parameterset, marks=[], id=None) @staticmethod - def _parse_parametrize_args(argnames, argvalues, **_): - """It receives an ignored _ (kwargs) argument so this function can - take also calls from parametrize ignoring scope, indirect, and other - arguments...""" + def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 6b26be72b89..df93d4ef538 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1761,3 +1761,16 @@ def test_func_b(y): result.stdout.fnmatch_lines( ["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"] ) + + def test_parametrize_positional_args(self, testdir): + testdir.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("a", [1], False) + def test_foo(a): + pass + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) From 23aa3bb0ae02bf138a5650f44d67b697cf405172 Mon Sep 17 00:00:00 2001 From: "Kevin J. Foley" Date: Mon, 24 Jun 2019 20:55:51 -0400 Subject: [PATCH 064/109] Clarify changelog entries should be rst files --- CONTRIBUTING.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 57628a34bdf..ec053c081ed 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -173,7 +173,7 @@ Short version The test environments above are usually enough to cover most cases locally. -#. Write a ``changelog`` entry: ``changelog/2574.bugfix``, use issue id number +#. Write a ``changelog`` entry: ``changelog/2574.bugfix.rst``, use issue id number and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``trivial`` for the issue type. #. Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please @@ -264,7 +264,7 @@ Here is a simple overview, with pytest-specific bits: $ git commit -a -m "" $ git push -u -#. Create a new changelog entry in ``changelog``. The file should be named ``.``, +#. Create a new changelog entry in ``changelog``. The file should be named ``..rst``, where *issueid* is the number of the issue related to the change and *type* is one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``trivial``. From 98b212cbfb2b58abeaa58609e53667058ba23429 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 10:35:09 +0200 Subject: [PATCH 065/109] Added "experimental" note. --- src/_pytest/hookspec.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 70301566caa..c22b4c12a17 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -497,6 +497,13 @@ def pytest_assertion_pass(item, lineno, orig, expl): :param int lineno: line number of the assert statement :param string orig: string with original assertion :param string expl: string with assert explanation + + .. note:: + + This hook is still *experimental*, so its parameters or even the hook itself might + be changed/removed without warning in any future pytest release. + + If you find this hook useful, please share your feedback opening an issue. """ From f8c9a7b86daaa265c934e7a775841aa98c734fcd Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 10:35:42 +0200 Subject: [PATCH 066/109] Formatting and removed py2 support. --- src/_pytest/assertion/rewrite.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 528c3500bb4..ca3f18cf322 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -807,10 +807,7 @@ def visit_Assert(self, assert_): fmt = self.helper("_format_explanation", err_msg) fmt_pass = self.helper("_format_explanation", msg) exc = ast.Call(err_name, [fmt], []) - if sys.version_info[0] >= 3: - raise_ = ast.Raise(exc, None) - else: - raise_ = ast.Raise(exc, None, None) + raise_ = ast.Raise(exc, None) # Call to hook when passes orig = astor.to_source(assert_.test).rstrip("\n").lstrip("(").rstrip(")") hook_call_pass = ast.Expr( @@ -821,9 +818,7 @@ def visit_Assert(self, assert_): # If any hooks implement assert_pass hook hook_impl_test = ast.If( - self.helper("_check_if_assertionpass_impl"), - [hook_call_pass], - [], + self.helper("_check_if_assertionpass_impl"), [hook_call_pass], [] ) main_test = ast.If(negation, [raise_], [hook_impl_test]) From 2280f28596b8443d5d3522b683d59bb6705ff2d8 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 10:36:01 +0200 Subject: [PATCH 067/109] Black formatting. --- src/_pytest/assertion/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index b59b1bfdffc..f670afe4f71 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -130,8 +130,12 @@ def callbinrepr(op, left, right): util._reprcompare = callbinrepr if item.ihook.pytest_assertion_pass.get_hookimpls(): + def call_assertion_pass_hook(lineno, expl, orig): - item.ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl) + item.ihook.pytest_assertion_pass( + item=item, lineno=lineno, orig=orig, expl=expl + ) + util._assertion_pass = call_assertion_pass_hook From 81e3f3cf95c95465ba515ae9fba2546cd8b6233a Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 10:41:11 +0200 Subject: [PATCH 068/109] Black formatting --- testing/test_assertrewrite.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8304cf05771..d3c50511ff4 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1339,9 +1339,9 @@ def test_hook_not_called_without_hookimpl(self, testdir, monkeypatch): def raise_on_assertionpass(*_, **__): raise Exception("Assertion passed called when it shouldn't!") - monkeypatch.setattr(_pytest.assertion.rewrite, - "_call_assertion_pass", - raise_on_assertionpass) + monkeypatch.setattr( + _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass + ) testdir.makepyfile( """ From b991810f32bc56ec8ab773eac81fccf69647ffb0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Jun 2019 08:00:20 -0700 Subject: [PATCH 069/109] Do not attempt to rewrite non-source files --- src/_pytest/assertion/rewrite.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index cce98000530..de33b6058f7 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -69,6 +69,8 @@ def find_spec(self, name, path=None, target=None): # python3.5 - python3.6: `namespace` # python3.7+: `None` or spec.origin in {None, "namespace"} + # we can only rewrite source files + or not isinstance(spec.loader, importlib.machinery.SourceFileLoader) # if the file doesn't exist, we can't rewrite it or not os.path.exists(spec.origin) ): From db50a975fd2377e28bafd82edbd90bc2e9aeeb1b Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 17:23:14 +0200 Subject: [PATCH 070/109] Reverted leak fixture test. --- testing/acceptance_test.py | 45 ++++++++++++++++++++++++ testing/fixture_values_leak_test.py | 53 ----------------------------- 2 files changed, 45 insertions(+), 53 deletions(-) delete mode 100644 testing/fixture_values_leak_test.py diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ccb69dd79d1..60cc21c4a01 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1047,6 +1047,51 @@ def test(request): result.stdout.fnmatch_lines(["* 1 passed *"]) +def test_fixture_values_leak(testdir): + """Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected + life-times (#2981). + """ + testdir.makepyfile( + """ + import attr + import gc + import pytest + import weakref + + @attr.s + class SomeObj(object): + name = attr.ib() + + fix_of_test1_ref = None + session_ref = None + + @pytest.fixture(scope='session') + def session_fix(): + global session_ref + obj = SomeObj(name='session-fixture') + session_ref = weakref.ref(obj) + return obj + + @pytest.fixture + def fix(session_fix): + global fix_of_test1_ref + obj = SomeObj(name='local-fixture') + fix_of_test1_ref = weakref.ref(obj) + return obj + + def test1(fix): + assert fix_of_test1_ref() is fix + + def test2(): + gc.collect() + # fixture "fix" created during test1 must have been destroyed by now + assert fix_of_test1_ref() is None + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["* 2 passed *"]) + + def test_fixture_order_respects_scope(testdir): """Ensure that fixtures are created according to scope order, regression test for #2405 """ diff --git a/testing/fixture_values_leak_test.py b/testing/fixture_values_leak_test.py deleted file mode 100644 index 6f4c90d3e07..00000000000 --- a/testing/fixture_values_leak_test.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected -life-times (#2981). - -This comes from the old acceptance_test.py::test_fixture_values_leak(testdir): -This used pytester before but was not working when using pytest_assert_reprcompare -because pytester tracks hook calls and it would hold a reference (ParsedCall object), -preventing garbage collection - -, - 'op': 'is', - 'left': SomeObj(name='local-fixture'), - 'right': SomeObj(name='local-fixture')})> -""" -import attr -import gc -import pytest -import weakref - - -@attr.s -class SomeObj(object): - name = attr.ib() - - -fix_of_test1_ref = None -session_ref = None - - -@pytest.fixture(scope="session") -def session_fix(): - global session_ref - obj = SomeObj(name="session-fixture") - session_ref = weakref.ref(obj) - return obj - - -@pytest.fixture -def fix(session_fix): - global fix_of_test1_ref - obj = SomeObj(name="local-fixture") - fix_of_test1_ref = weakref.ref(obj) - return obj - - -def test1(fix): - assert fix_of_test1_ref() is fix - - -def test2(): - gc.collect() - # fixture "fix" created during test1 must have been destroyed by now - assert fix_of_test1_ref() is None From cfbfa53f2b0d38de84928fba8a8709003162ea6d Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 17:46:56 +0200 Subject: [PATCH 071/109] Using pytester subprocess to avoid keeping references in the HookRecorder. --- testing/acceptance_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 60cc21c4a01..3f339366e52 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1088,7 +1088,10 @@ def test2(): assert fix_of_test1_ref() is None """ ) - result = testdir.runpytest() + # Running on subprocess does not activate the HookRecorder + # which holds itself a reference to objects in case of the + # pytest_assert_reprcompare hook + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["* 2 passed *"]) From 4db5488ed888a4cd20cd458405ad6cf4dddca476 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 19:49:05 +0200 Subject: [PATCH 072/109] Now dependent on command line option. --- src/_pytest/assertion/__init__.py | 8 +++ src/_pytest/assertion/rewrite.py | 101 +++++++++++++++++++----------- src/_pytest/config/__init__.py | 6 +- src/_pytest/hookspec.py | 2 + testing/test_assertrewrite.py | 35 ++++++++++- 5 files changed, 113 insertions(+), 39 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index f670afe4f71..9e53c79f4d2 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -24,6 +24,14 @@ def pytest_addoption(parser): expression information.""", ) + group = parser.getgroup("experimental") + group.addoption( + "--enable-assertion-pass-hook", + action="store_true", + help="Enables the pytest_assertion_pass hook." + "Make sure to delete any previously generated pyc cache files.", + ) + def register_assert_rewrite(*names): """Register one or more module names to be rewritten on import. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ca3f18cf322..5477927b761 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -745,7 +745,8 @@ def pop_format_context(self, expl_expr): format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) - self.format_variables.append(name) + if getattr(self.config._ns, "enable_assertion_pass_hook", False): + self.format_variables.append(name) self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) return ast.Name(name, ast.Load()) @@ -780,7 +781,10 @@ def visit_Assert(self, assert_): self.statements = [] self.variables = [] self.variable_counter = itertools.count() - self.format_variables = [] + + if getattr(self.config._ns, "enable_assertion_pass_hook", False): + self.format_variables = [] + self.stack = [] self.expl_stmts = [] self.push_format_context() @@ -793,41 +797,68 @@ def visit_Assert(self, assert_): top_condition, module_path=self.module_path, lineno=assert_.lineno ) ) - negation = ast.UnaryOp(ast.Not(), top_condition) - msg = self.pop_format_context(ast.Str(explanation)) - if assert_.msg: - assertmsg = self.helper("_format_assertmsg", assert_.msg) - gluestr = "\n>assert " - else: - assertmsg = ast.Str("") - gluestr = "assert " - err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) - err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) - err_name = ast.Name("AssertionError", ast.Load()) - fmt = self.helper("_format_explanation", err_msg) - fmt_pass = self.helper("_format_explanation", msg) - exc = ast.Call(err_name, [fmt], []) - raise_ = ast.Raise(exc, None) - # Call to hook when passes - orig = astor.to_source(assert_.test).rstrip("\n").lstrip("(").rstrip(")") - hook_call_pass = ast.Expr( - self.helper( - "_call_assertion_pass", ast.Num(assert_.lineno), ast.Str(orig), fmt_pass + if getattr(self.config._ns, "enable_assertion_pass_hook", False): + ### Experimental pytest_assertion_pass hook + negation = ast.UnaryOp(ast.Not(), top_condition) + msg = self.pop_format_context(ast.Str(explanation)) + if assert_.msg: + assertmsg = self.helper("_format_assertmsg", assert_.msg) + gluestr = "\n>assert " + else: + assertmsg = ast.Str("") + gluestr = "assert " + err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) + err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) + err_name = ast.Name("AssertionError", ast.Load()) + fmt = self.helper("_format_explanation", err_msg) + fmt_pass = self.helper("_format_explanation", msg) + exc = ast.Call(err_name, [fmt], []) + raise_ = ast.Raise(exc, None) + # Call to hook when passes + orig = astor.to_source(assert_.test).rstrip("\n").lstrip("(").rstrip(")") + hook_call_pass = ast.Expr( + self.helper( + "_call_assertion_pass", + ast.Num(assert_.lineno), + ast.Str(orig), + fmt_pass, + ) ) - ) - # If any hooks implement assert_pass hook - hook_impl_test = ast.If( - self.helper("_check_if_assertionpass_impl"), [hook_call_pass], [] - ) - main_test = ast.If(negation, [raise_], [hook_impl_test]) - - self.statements.extend(self.expl_stmts) - self.statements.append(main_test) - if self.format_variables: - variables = [ast.Name(name, ast.Store()) for name in self.format_variables] - clear_format = ast.Assign(variables, _NameConstant(None)) - self.statements.append(clear_format) + # If any hooks implement assert_pass hook + hook_impl_test = ast.If( + self.helper("_check_if_assertionpass_impl"), [hook_call_pass], [] + ) + main_test = ast.If(negation, [raise_], [hook_impl_test]) + + self.statements.extend(self.expl_stmts) + self.statements.append(main_test) + if self.format_variables: + variables = [ + ast.Name(name, ast.Store()) for name in self.format_variables + ] + clear_format = ast.Assign(variables, _NameConstant(None)) + self.statements.append(clear_format) + else: + ### Original assertion rewriting + # Create failure message. + body = self.expl_stmts + negation = ast.UnaryOp(ast.Not(), top_condition) + self.statements.append(ast.If(negation, body, [])) + if assert_.msg: + assertmsg = self.helper("_format_assertmsg", assert_.msg) + explanation = "\n>assert " + explanation + else: + assertmsg = ast.Str("") + explanation = "assert " + explanation + template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) + 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], []) + raise_ = ast.Raise(exc, None) + + body.append(raise_) # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e6de86c3619..f947a4c3255 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -746,8 +746,10 @@ def _consider_importhook(self, args): and find all the installed plugins to mark them for rewriting by the importhook. """ - ns, unknown_args = self._parser.parse_known_and_unknown_args(args) - mode = getattr(ns, "assertmode", "plain") + # Saving _ns so it can be used for other assertion rewriting purposes + # e.g. experimental assertion pass hook + self._ns, self._unknown_args = self._parser.parse_known_and_unknown_args(args) + mode = getattr(self._ns, "assertmode", "plain") if mode == "rewrite": try: hook = _pytest.assertion.install_importhook(self) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index c22b4c12a17..5cb1d9ce5a6 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -503,6 +503,8 @@ def pytest_assertion_pass(item, lineno, orig, expl): This hook is still *experimental*, so its parameters or even the hook itself might be changed/removed without warning in any future pytest release. + It should be enabled using the `--enable-assertion-pass-hook` command line option. + If you find this hook useful, please share your feedback opening an issue. """ diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index d3c50511ff4..e74e6df8395 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1326,8 +1326,7 @@ def test_simple(): assert a+b == c+d """ ) - result = testdir.runpytest() - print(testdir.tmpdir) + result = testdir.runpytest("--enable-assertion-pass-hook") result.stdout.fnmatch_lines( "*Assertion Passed: a + b == c + d (1 + 2) == (3 + 0) at line 7*" ) @@ -1343,6 +1342,38 @@ def raise_on_assertionpass(*_, **__): _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass ) + testdir.makepyfile( + """ + def test_simple(): + a=1 + b=2 + c=3 + d=0 + + assert a+b == c+d + """ + ) + result = testdir.runpytest("--enable-assertion-pass-hook") + result.assert_outcomes(passed=1) + + def test_hook_not_called_without_cmd_option(self, testdir, monkeypatch): + """Assertion pass should not be called (and hence formatting should + not occur) if there is no hook declared for pytest_assertion_pass""" + + def raise_on_assertionpass(*_, **__): + raise Exception("Assertion passed called when it shouldn't!") + + monkeypatch.setattr( + _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass + ) + + testdir.makeconftest( + """ + def pytest_assertion_pass(item, lineno, orig, expl): + raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno)) + """ + ) + testdir.makepyfile( """ def test_simple(): From 80ac910a24ab8bfcf8b1821d459a7a4b98ece3f8 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Tue, 25 Jun 2019 19:49:57 +0200 Subject: [PATCH 073/109] Added msg to docstring for cleaning pyc. --- src/_pytest/hookspec.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 5cb1d9ce5a6..3b1aa277d20 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -504,6 +504,7 @@ def pytest_assertion_pass(item, lineno, orig, expl): be changed/removed without warning in any future pytest release. It should be enabled using the `--enable-assertion-pass-hook` command line option. + Remember to clean the .pyc files in your project directory and interpreter libraries. If you find this hook useful, please share your feedback opening an issue. """ From bd647fdd8b280e2753e07edd82170e00bcf841e0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Jun 2019 14:50:07 -0700 Subject: [PATCH 074/109] Revert allow_abbrev=False in helper scripts --- extra/get_issues.py | 2 +- scripts/release.py | 2 +- .../perf_examples/collect_stats/generate_folders.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extra/get_issues.py b/extra/get_issues.py index ae99c9aa60e..9407aeded7d 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -74,7 +74,7 @@ def report(issues): if __name__ == "__main__": import argparse - parser = argparse.ArgumentParser("process bitbucket issues", allow_abbrev=False) + parser = argparse.ArgumentParser("process bitbucket issues") parser.add_argument( "--refresh", action="store_true", help="invalidate cache, refresh issues" ) diff --git a/scripts/release.py b/scripts/release.py index d2a51e25a4f..5009df359e6 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -105,7 +105,7 @@ def changelog(version, write_out=False): def main(): init(autoreset=True) - parser = argparse.ArgumentParser(allow_abbrev=False) + parser = argparse.ArgumentParser() parser.add_argument("version", help="Release version") options = parser.parse_args() pre_release(options.version) diff --git a/testing/example_scripts/perf_examples/collect_stats/generate_folders.py b/testing/example_scripts/perf_examples/collect_stats/generate_folders.py index d2c1a30b2bb..ff1eaf7d6bb 100644 --- a/testing/example_scripts/perf_examples/collect_stats/generate_folders.py +++ b/testing/example_scripts/perf_examples/collect_stats/generate_folders.py @@ -4,7 +4,7 @@ HERE = pathlib.Path(__file__).parent TEST_CONTENT = (HERE / "template_test.py").read_bytes() -parser = argparse.ArgumentParser(allow_abbrev=False) +parser = argparse.ArgumentParser() parser.add_argument("numbers", nargs="*", type=int) From ed85c831548f4bae256b9ce90973ab74bf95c59f Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 25 Jun 2019 13:06:02 +1000 Subject: [PATCH 075/109] Deprecate funcargnames alias --- changelog/466.deprecation.rst | 2 ++ doc/en/deprecations.rst | 15 +++++++++++++++ src/_pytest/compat.py | 4 ++++ src/_pytest/deprecated.py | 5 +++++ src/_pytest/fixtures.py | 2 +- testing/python/fixtures.py | 11 +++++++---- testing/python/metafunc.py | 2 +- 7 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 changelog/466.deprecation.rst diff --git a/changelog/466.deprecation.rst b/changelog/466.deprecation.rst new file mode 100644 index 00000000000..65775c3862d --- /dev/null +++ b/changelog/466.deprecation.rst @@ -0,0 +1,2 @@ +The ``funcargnames`` attribute has been an alias for ``fixturenames`` since +pytest 2.3, and is now deprecated in code too. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 6b4f360b581..e2399dd41d0 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -19,6 +19,21 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. + +Removal of ``funcargnames`` alias for ``fixturenames`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.0 + +The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of +their associated fixtures, with the aptly-named ``fixturenames`` attribute. + +Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept +that as an alias since. It is finally due for removal, as it is often confusing +in places where we or plugin authors must distinguish between fixture names and +names supplied by non-fixture things such as ``pytest.mark.parametrize``. + + .. _`raises message deprecated`: ``"message"`` parameter of ``pytest.raises`` diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 2c964f473aa..d238061b4ab 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -325,4 +325,8 @@ class FuncargnamesCompatAttr: @property def funcargnames(self): """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + import warnings + from _pytest.deprecated import FUNCARGNAMES + + warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 1c544fd3681..e31b9eb0ed9 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -40,6 +40,11 @@ "getfuncargvalue is deprecated, use getfixturevalue" ) +FUNCARGNAMES = PytestDeprecationWarning( + "The `funcargnames` attribute was an alias for `fixturenames`, " + "since pytest 2.3 - use the newer attribute instead." +) + RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning( "The 'message' parameter is deprecated.\n" "(did you mean to use `match='some regex'` to check the exception message?)\n" diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0a792d11d2c..3262b65bb55 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -654,7 +654,7 @@ def _schedule_finalizers(self, fixturedef, subrequest): # if the executing fixturedef was not explicitly requested in the argument list (via # getfixturevalue inside the fixture call) then ensure this fixture def will be finished # first - if fixturedef.argname not in self.funcargnames: + if fixturedef.argname not in self.fixturenames: fixturedef.addfinalizer( functools.partial(self._fixturedef.finish, request=self) ) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 1d39079ea92..c0c230ccf2b 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -793,12 +793,15 @@ def test_funcargnames_compatattr(self, testdir): """ import pytest def pytest_generate_tests(metafunc): - assert metafunc.funcargnames == metafunc.fixturenames + with pytest.warns(pytest.PytestDeprecationWarning): + assert metafunc.funcargnames == metafunc.fixturenames @pytest.fixture def fn(request): - assert request._pyfuncitem.funcargnames == \ - request._pyfuncitem.fixturenames - return request.funcargnames, request.fixturenames + with pytest.warns(pytest.PytestDeprecationWarning): + assert request._pyfuncitem.funcargnames == \ + request._pyfuncitem.fixturenames + with pytest.warns(pytest.PytestDeprecationWarning): + return request.funcargnames, request.fixturenames def test_hello(fn): assert fn[0] == fn[1] diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index df93d4ef538..542557252fe 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1205,7 +1205,7 @@ def test_parametrize_scope_overrides(self, testdir, scope, length): import pytest values = [] def pytest_generate_tests(metafunc): - if "arg" in metafunc.funcargnames: + if "arg" in metafunc.fixturenames: metafunc.parametrize("arg", [1,2], indirect=True, scope=%r) @pytest.fixture From 8c7eb8236348c8fb8db43a6a9c02ba442a09f6b8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 May 2019 12:03:00 +0200 Subject: [PATCH 076/109] Fix/improve comparison of byte strings Fixes https://github.com/pytest-dev/pytest/issues/5260. --- changelog/5260.bugfix.rst | 1 + src/_pytest/assertion/util.py | 5 ++++- testing/test_assertion.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelog/5260.bugfix.rst diff --git a/changelog/5260.bugfix.rst b/changelog/5260.bugfix.rst new file mode 100644 index 00000000000..ab521d163fc --- /dev/null +++ b/changelog/5260.bugfix.rst @@ -0,0 +1 @@ +Improve/fix comparison of byte strings with Python 3. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 762e5761d71..493c630f614 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -263,8 +263,11 @@ def _compare_eq_sequence(left, right, verbose=0): "At index {} diff: {!r} != {!r}".format(i, left[i], right[i]) ] break - len_diff = len_left - len_right + if isinstance(left, bytes): + return explanation + + len_diff = len_left - len_right if len_diff: if len_diff > 0: dir_with_more = "Left" diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 0fcfd9f27e1..f9c67d70310 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -331,6 +331,18 @@ def test_multiline_text_diff(self): assert "- spam" in diff assert "+ eggs" in diff + def test_bytes_diff(self): + diff = callequal(b"spam", b"eggs") + if PY3: + assert diff == [ + "b'spam' == b'eggs'", + "At index 0 diff: 115 != 101", + "Use -v to get the full diff", + ] + else: + # Handled as text on Python 2. + assert diff == ["'spam' == 'eggs'", "- spam", "+ eggs"] + def test_list(self): expl = callequal([0, 1], [0, 2]) assert len(expl) > 1 From 3f2344e8f73784b40df28d82a44249827206dc63 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 25 Jun 2019 20:30:18 -0300 Subject: [PATCH 077/109] Show bytes ascii representation instead of numeric value --- changelog/5260.bugfix.rst | 18 +++++++++++++++++- src/_pytest/assertion/util.py | 22 ++++++++++++++++++++-- testing/test_assertion.py | 29 +++++++++++++++++++---------- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/changelog/5260.bugfix.rst b/changelog/5260.bugfix.rst index ab521d163fc..484c1438ae6 100644 --- a/changelog/5260.bugfix.rst +++ b/changelog/5260.bugfix.rst @@ -1 +1,17 @@ -Improve/fix comparison of byte strings with Python 3. +Improved comparison of byte strings. + +When comparing bytes, the assertion message used to show the byte numeric value when showing the differences:: + + def test(): + > assert b'spam' == b'eggs' + E AssertionError: assert b'spam' == b'eggs' + E At index 0 diff: 115 != 101 + E Use -v to get the full diff + +It now shows the actual ascii representation instead, which is often more useful:: + + def test(): + > assert b'spam' == b'eggs' + E AssertionError: assert b'spam' == b'eggs' + E At index 0 diff: b's' != b'e' + E Use -v to get the full diff diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 493c630f614..b808cb50980 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -254,17 +254,35 @@ def _compare_eq_iterable(left, right, verbose=0): def _compare_eq_sequence(left, right, verbose=0): + comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) explanation = [] len_left = len(left) len_right = len(right) for i in range(min(len_left, len_right)): if left[i] != right[i]: + if comparing_bytes: + # when comparing bytes, we want to see their ascii representation + # instead of their numeric values (#5260) + # using a slice gives us the ascii representation: + # >>> s = b'foo' + # >>> s[0] + # 102 + # >>> s[0:1] + # b'f' + left_value = left[i : i + 1] + right_value = right[i : i + 1] + else: + left_value = left[i] + right_value = right[i] + explanation += [ - "At index {} diff: {!r} != {!r}".format(i, left[i], right[i]) + "At index {} diff: {!r} != {!r}".format(i, left_value, right_value) ] break - if isinstance(left, bytes): + if comparing_bytes: + # when comparing bytes, it doesn't help to show the "sides contain one or more items" + # longer explanation, so skip it return explanation len_diff = len_left - len_right diff --git a/testing/test_assertion.py b/testing/test_assertion.py index f9c67d70310..f58d240a57f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -331,17 +331,26 @@ def test_multiline_text_diff(self): assert "- spam" in diff assert "+ eggs" in diff - def test_bytes_diff(self): + def test_bytes_diff_normal(self): + """Check special handling for bytes diff (#5260)""" diff = callequal(b"spam", b"eggs") - if PY3: - assert diff == [ - "b'spam' == b'eggs'", - "At index 0 diff: 115 != 101", - "Use -v to get the full diff", - ] - else: - # Handled as text on Python 2. - assert diff == ["'spam' == 'eggs'", "- spam", "+ eggs"] + + assert diff == [ + "b'spam' == b'eggs'", + "At index 0 diff: b's' != b'e'", + "Use -v to get the full diff", + ] + + def test_bytes_diff_verbose(self): + """Check special handling for bytes diff (#5260)""" + diff = callequal(b"spam", b"eggs", verbose=True) + assert diff == [ + "b'spam' == b'eggs'", + "At index 0 diff: b's' != b'e'", + "Full diff:", + "- b'spam'", + "+ b'eggs'", + ] def test_list(self): expl = callequal([0, 1], [0, 2]) From bfba33ec9e8f4c55d3184294473ade83c6300104 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 25 Jun 2019 20:24:13 -0700 Subject: [PATCH 078/109] Delete stray comment --- src/pytest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pytest.py b/src/pytest.py index a3fa260845d..b4faf49786e 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -2,7 +2,6 @@ """ pytest: unit and functional testing with Python. """ -# else we are imported from _pytest import __version__ from _pytest.assertion import register_assert_rewrite from _pytest.config import cmdline From 7efdd5063bb0a64e09b0f3db3a109474d78c2132 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 10:50:27 +0200 Subject: [PATCH 079/109] Update src/_pytest/assertion/rewrite.py Co-Authored-By: Bruno Oliveira --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 5477927b761..4d99c1a7887 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -484,7 +484,7 @@ def _call_assertion_pass(lineno, orig, expl): util._assertion_pass(lineno=lineno, orig=orig, expl=expl) -def _check_if_assertionpass_impl(): +def _check_if_assertion_pass_impl(): """Checks if any plugins implement the pytest_assertion_pass hook in order not to generate explanation unecessarily (might be expensive)""" return True if util._assertion_pass else False From fdb6e35b1b9b19d53e00adc2f726140693702f3e Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 26 Jun 2019 20:23:35 +1000 Subject: [PATCH 080/109] Fix typo replace `circuting` with `circuiting`. --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index f50d8200e89..374bb2518b8 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -771,7 +771,7 @@ def visit_BoolOp(self, boolop): fail_save = self.on_failure levels = len(boolop.values) - 1 self.push_format_context() - # Process each operand, short-circuting if needed. + # Process each operand, short-circuiting if needed. for i, v in enumerate(boolop.values): if i: fail_inner = [] From d81f758285499a8953e874b2b53ba8a5b1fa7430 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 26 Jun 2019 20:31:45 +1000 Subject: [PATCH 081/109] Update changelog with trivial as per ./CONTRIBUTING.rst --- changelog/5497.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5497.trivial.rst diff --git a/changelog/5497.trivial.rst b/changelog/5497.trivial.rst new file mode 100644 index 00000000000..735d1f9286b --- /dev/null +++ b/changelog/5497.trivial.rst @@ -0,0 +1 @@ +Fix typo replace `circuting` with `circuiting`. From 994c32235c04480415a0eae55c8eab7332cc5f07 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 26 Jun 2019 20:46:09 +1000 Subject: [PATCH 082/109] Fix rst support --- changelog/5497.trivial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/5497.trivial.rst b/changelog/5497.trivial.rst index 735d1f9286b..4cd2087bf6f 100644 --- a/changelog/5497.trivial.rst +++ b/changelog/5497.trivial.rst @@ -1 +1 @@ -Fix typo replace `circuting` with `circuiting`. +Fix typo replace ``circuting`` with circuiting. From a48feb3261c46ecda88fb97172d84f9f22ec1a88 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 11:09:04 -0300 Subject: [PATCH 083/109] Delete 5497.trivial.rst Just a typo, no need for a changelog entry. :) --- changelog/5497.trivial.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog/5497.trivial.rst diff --git a/changelog/5497.trivial.rst b/changelog/5497.trivial.rst deleted file mode 100644 index 4cd2087bf6f..00000000000 --- a/changelog/5497.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fix typo replace ``circuting`` with circuiting. From 0fb52416b112ced523757471d4599cfd7340d2f4 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 17:00:37 +0200 Subject: [PATCH 084/109] Reverted changes. --- src/_pytest/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f947a4c3255..437b38853cd 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -748,8 +748,8 @@ def _consider_importhook(self, args): """ # Saving _ns so it can be used for other assertion rewriting purposes # e.g. experimental assertion pass hook - self._ns, self._unknown_args = self._parser.parse_known_and_unknown_args(args) - mode = getattr(self._ns, "assertmode", "plain") + _ns, _unknown_args = self._parser.parse_known_and_unknown_args(args) + mode = getattr(_ns, "assertmode", "plain") if mode == "rewrite": try: hook = _pytest.assertion.install_importhook(self) From d638da5821246caf1782a1a0519308987befad47 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 17:08:09 +0200 Subject: [PATCH 085/109] Using ini-file option instead of cmd option. --- src/_pytest/assertion/__init__.py | 9 ++++----- src/_pytest/assertion/rewrite.py | 10 +++++++--- src/_pytest/hookspec.py | 2 +- testing/test_assertrewrite.py | 21 +++++++++++++++++++-- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 9e53c79f4d2..53b33fe3836 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -23,11 +23,10 @@ def pytest_addoption(parser): test modules on import to provide assert expression information.""", ) - - group = parser.getgroup("experimental") - group.addoption( - "--enable-assertion-pass-hook", - action="store_true", + parser.addini( + "enable_assertion_pass_hook", + type="bool", + default="False", help="Enables the pytest_assertion_pass hook." "Make sure to delete any previously generated pyc cache files.", ) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 4d99c1a7887..1a41f662706 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -604,6 +604,10 @@ def __init__(self, module_path, config): super().__init__() self.module_path = module_path self.config = config + if config is not None: + self.enable_assertion_pass_hook = config.getini("enable_assertion_pass_hook") + else: + self.enable_assertion_pass_hook = False def run(self, mod): """Find all assert statements in *mod* and rewrite them.""" @@ -745,7 +749,7 @@ def pop_format_context(self, expl_expr): format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) - if getattr(self.config._ns, "enable_assertion_pass_hook", False): + if self.enable_assertion_pass_hook: self.format_variables.append(name) self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) return ast.Name(name, ast.Load()) @@ -782,7 +786,7 @@ def visit_Assert(self, assert_): self.variables = [] self.variable_counter = itertools.count() - if getattr(self.config._ns, "enable_assertion_pass_hook", False): + if self.enable_assertion_pass_hook: self.format_variables = [] self.stack = [] @@ -797,7 +801,7 @@ def visit_Assert(self, assert_): top_condition, module_path=self.module_path, lineno=assert_.lineno ) ) - if getattr(self.config._ns, "enable_assertion_pass_hook", False): + if self.enable_assertion_pass_hook: ### Experimental pytest_assertion_pass hook negation = ast.UnaryOp(ast.Not(), top_condition) msg = self.pop_format_context(ast.Str(explanation)) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 3b1aa277d20..268348eac05 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -503,7 +503,7 @@ def pytest_assertion_pass(item, lineno, orig, expl): This hook is still *experimental*, so its parameters or even the hook itself might be changed/removed without warning in any future pytest release. - It should be enabled using the `--enable-assertion-pass-hook` command line option. + It should be enabled using the `enable_assertion_pass_hook` ini-file option. Remember to clean the .pyc files in your project directory and interpreter libraries. If you find this hook useful, please share your feedback opening an issue. diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e74e6df8395..040972791c7 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1308,6 +1308,7 @@ def test(): class TestAssertionPass: + def test_hook_call(self, testdir): testdir.makeconftest( """ @@ -1315,6 +1316,12 @@ def pytest_assertion_pass(item, lineno, orig, expl): raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno)) """ ) + + testdir.makeini(""" + [pytest] + enable_assertion_pass_hook = True + """) + testdir.makepyfile( """ def test_simple(): @@ -1326,7 +1333,7 @@ def test_simple(): assert a+b == c+d """ ) - result = testdir.runpytest("--enable-assertion-pass-hook") + result = testdir.runpytest() result.stdout.fnmatch_lines( "*Assertion Passed: a + b == c + d (1 + 2) == (3 + 0) at line 7*" ) @@ -1342,6 +1349,11 @@ def raise_on_assertionpass(*_, **__): _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass ) + testdir.makeini(""" + [pytest] + enable_assertion_pass_hook = True + """) + testdir.makepyfile( """ def test_simple(): @@ -1353,7 +1365,7 @@ def test_simple(): assert a+b == c+d """ ) - result = testdir.runpytest("--enable-assertion-pass-hook") + result = testdir.runpytest() result.assert_outcomes(passed=1) def test_hook_not_called_without_cmd_option(self, testdir, monkeypatch): @@ -1374,6 +1386,11 @@ def pytest_assertion_pass(item, lineno, orig, expl): """ ) + testdir.makeini(""" + [pytest] + enable_assertion_pass_hook = False + """) + testdir.makepyfile( """ def test_simple(): From f755ff6af15384b8807d31ba2c3e3028d1966104 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 17:08:35 +0200 Subject: [PATCH 086/109] Black formatting. --- src/_pytest/assertion/rewrite.py | 4 +++- testing/test_assertrewrite.py | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 1a41f662706..25e1ce0750d 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -605,7 +605,9 @@ def __init__(self, module_path, config): self.module_path = module_path self.config = config if config is not None: - self.enable_assertion_pass_hook = config.getini("enable_assertion_pass_hook") + self.enable_assertion_pass_hook = config.getini( + "enable_assertion_pass_hook" + ) else: self.enable_assertion_pass_hook = False diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 040972791c7..129eca6806d 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1308,7 +1308,6 @@ def test(): class TestAssertionPass: - def test_hook_call(self, testdir): testdir.makeconftest( """ @@ -1317,10 +1316,12 @@ def pytest_assertion_pass(item, lineno, orig, expl): """ ) - testdir.makeini(""" + testdir.makeini( + """ [pytest] enable_assertion_pass_hook = True - """) + """ + ) testdir.makepyfile( """ @@ -1349,10 +1350,12 @@ def raise_on_assertionpass(*_, **__): _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass ) - testdir.makeini(""" + testdir.makeini( + """ [pytest] enable_assertion_pass_hook = True - """) + """ + ) testdir.makepyfile( """ @@ -1386,10 +1389,12 @@ def pytest_assertion_pass(item, lineno, orig, expl): """ ) - testdir.makeini(""" + testdir.makeini( + """ [pytest] enable_assertion_pass_hook = False - """) + """ + ) testdir.makepyfile( """ From d91a5d3cd73a390f7cc5b03c56678f66aeab5853 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 17:32:35 +0200 Subject: [PATCH 087/109] Further reverting changes. --- src/_pytest/config/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 437b38853cd..e6de86c3619 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -746,10 +746,8 @@ def _consider_importhook(self, args): and find all the installed plugins to mark them for rewriting by the importhook. """ - # Saving _ns so it can be used for other assertion rewriting purposes - # e.g. experimental assertion pass hook - _ns, _unknown_args = self._parser.parse_known_and_unknown_args(args) - mode = getattr(_ns, "assertmode", "plain") + ns, unknown_args = self._parser.parse_known_and_unknown_args(args) + mode = getattr(ns, "assertmode", "plain") if mode == "rewrite": try: hook = _pytest.assertion.install_importhook(self) From 9a34d88c8d369edae357094816b6aeef4d3a914d Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 18:09:51 +0200 Subject: [PATCH 088/109] Explanation variables only defined if failed or passed with plugins implementing the hook. --- src/_pytest/assertion/rewrite.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 25e1ce0750d..68fe8fd0940 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -803,10 +803,12 @@ def visit_Assert(self, assert_): top_condition, module_path=self.module_path, lineno=assert_.lineno ) ) - if self.enable_assertion_pass_hook: - ### Experimental pytest_assertion_pass hook + + if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook negation = ast.UnaryOp(ast.Not(), top_condition) msg = self.pop_format_context(ast.Str(explanation)) + + # Failed if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) gluestr = "\n>assert " @@ -817,10 +819,14 @@ def visit_Assert(self, assert_): err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_name = ast.Name("AssertionError", ast.Load()) fmt = self.helper("_format_explanation", err_msg) - fmt_pass = self.helper("_format_explanation", msg) exc = ast.Call(err_name, [fmt], []) raise_ = ast.Raise(exc, None) - # Call to hook when passes + statements_fail = [] + statements_fail.extend(self.expl_stmts) + statements_fail.append(raise_) + + # Passed + fmt_pass = self.helper("_format_explanation", msg) orig = astor.to_source(assert_.test).rstrip("\n").lstrip("(").rstrip(")") hook_call_pass = ast.Expr( self.helper( @@ -830,14 +836,16 @@ def visit_Assert(self, assert_): fmt_pass, ) ) - # If any hooks implement assert_pass hook hook_impl_test = ast.If( self.helper("_check_if_assertionpass_impl"), [hook_call_pass], [] ) - main_test = ast.If(negation, [raise_], [hook_impl_test]) + statements_pass = [] + statements_pass.extend(self.expl_stmts) + statements_pass.append(hook_impl_test) - self.statements.extend(self.expl_stmts) + # Test for assertion condition + main_test = ast.If(negation, statements_fail, statements_pass) self.statements.append(main_test) if self.format_variables: variables = [ @@ -845,8 +853,8 @@ def visit_Assert(self, assert_): ] clear_format = ast.Assign(variables, _NameConstant(None)) self.statements.append(clear_format) - else: - ### Original assertion rewriting + + else: # Original assertion rewriting # Create failure message. body = self.expl_stmts negation = ast.UnaryOp(ast.Not(), top_condition) @@ -865,6 +873,7 @@ def visit_Assert(self, assert_): raise_ = ast.Raise(exc, None) body.append(raise_) + # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] From 53234bf6136c3821df917cf89507ed4cb5fde476 Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 19:00:31 +0200 Subject: [PATCH 089/109] Added config back to AssertionWriter and fixed typo in check_if_assertion_pass_impl function call. --- src/_pytest/assertion/rewrite.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 2afe76b82a0..a79a5215728 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -135,7 +135,7 @@ def exec_module(self, module): co = _read_pyc(fn, pyc, state.trace) if co is None: state.trace("rewriting {!r}".format(fn)) - source_stat, co = _rewrite_test(fn) + source_stat, co = _rewrite_test(fn, self.config) if write: self._writing_pyc = True try: @@ -279,13 +279,13 @@ def _write_pyc(state, co, source_stat, pyc): return True -def _rewrite_test(fn): +def _rewrite_test(fn, config): """read and rewrite *fn* and return the code object.""" stat = os.stat(fn) with open(fn, "rb") as f: source = f.read() tree = ast.parse(source, filename=fn) - rewrite_asserts(tree, fn) + rewrite_asserts(tree, fn, config) co = compile(tree, fn, "exec", dont_inherit=True) return stat, co @@ -327,9 +327,9 @@ def _read_pyc(source, pyc, trace=lambda x: None): return co -def rewrite_asserts(mod, module_path=None): +def rewrite_asserts(mod, module_path=None, config=None): """Rewrite the assert statements in mod.""" - AssertionRewriter(module_path).run(mod) + AssertionRewriter(module_path, config).run(mod) def _saferepr(obj): @@ -523,7 +523,7 @@ class AssertionRewriter(ast.NodeVisitor): """ - def __init__(self, module_path): + def __init__(self, module_path, config): super().__init__() self.module_path = module_path self.config = config @@ -761,7 +761,7 @@ def visit_Assert(self, assert_): ) # If any hooks implement assert_pass hook hook_impl_test = ast.If( - self.helper("_check_if_assertionpass_impl"), [hook_call_pass], [] + self.helper("_check_if_assertion_pass_impl"), [hook_call_pass], [] ) statements_pass = [] statements_pass.extend(self.expl_stmts) From 6854ff2acc1f291d4781d97bf78d563bb2113c5e Mon Sep 17 00:00:00 2001 From: Victor Maryama Date: Wed, 26 Jun 2019 19:05:17 +0200 Subject: [PATCH 090/109] Fixed import order pep8. --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index a79a5215728..62d8dbb6ac5 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1,6 +1,5 @@ """Rewrite assertion AST to produce nice error messages""" import ast -import astor import errno import importlib.machinery import importlib.util @@ -11,6 +10,7 @@ import sys import types +import astor import atomicwrites from _pytest._io.saferepr import saferepr From eb90f3d1c89a10a00b5cd34a5df3aaaaec500177 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 17:54:24 -0300 Subject: [PATCH 091/109] Fix default value of 'enable_assertion_pass_hook' --- src/_pytest/assertion/__init__.py | 2 +- testing/test_assertrewrite.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 53b33fe3836..126929b6ad9 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -26,7 +26,7 @@ def pytest_addoption(parser): parser.addini( "enable_assertion_pass_hook", type="bool", - default="False", + default=False, help="Enables the pytest_assertion_pass hook." "Make sure to delete any previously generated pyc cache files.", ) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 0c01be28c98..5c680927924 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1335,6 +1335,10 @@ def test(): class TestAssertionPass: + def test_option_default(self, testdir): + config = testdir.parseconfig() + assert config.getini("enable_assertion_pass_hook") is False + def test_hook_call(self, testdir): testdir.makeconftest( """ From fcbe66feba83b5991d093cc5e42a46001e46cab0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 18:51:14 -0300 Subject: [PATCH 092/109] Restore proper handling of '%' in assertion messages --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 62d8dbb6ac5..7b098ad1b52 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -358,7 +358,7 @@ def _format_assertmsg(obj): # contains a newline it gets escaped, however if an object has a # .__repr__() which contains newlines it does not get escaped. # However in either case we want to preserve the newline. - replaces = [("\n", "\n~")] + replaces = [("\n", "\n~"), ("%", "%%")] if not isinstance(obj, str): obj = saferepr(obj) replaces.append(("\\n", "\n~")) From 3afee36ebb0720bb15f4ca67ea0f1305434e1cc7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 19:10:54 -0300 Subject: [PATCH 093/109] Improve docs and reference --- changelog/3457.feature.rst | 6 ++++-- doc/en/reference.rst | 7 +++---- src/_pytest/hookspec.py | 25 ++++++++++++++++++------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/changelog/3457.feature.rst b/changelog/3457.feature.rst index 3f676514431..c309430706c 100644 --- a/changelog/3457.feature.rst +++ b/changelog/3457.feature.rst @@ -1,2 +1,4 @@ -Adds ``pytest_assertion_pass`` hook, called with assertion context information -(original asssertion statement and pytest explanation) whenever an assertion passes. +New `pytest_assertion_pass `__ +hook, called with context information when an assertion *passes*. + +This hook is still **experimental** so use it with caution. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 6750b17f082..5abb01f5023 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -665,15 +665,14 @@ Session related reporting hooks: .. autofunction:: pytest_fixture_post_finalizer .. autofunction:: pytest_warning_captured -And here is the central hook for reporting about -test execution: +Central hook for reporting about test execution: .. autofunction:: pytest_runtest_logreport -You can also use this hook to customize assertion representation for some -types: +Assertion related hooks: .. autofunction:: pytest_assertrepr_compare +.. autofunction:: pytest_assertion_pass Debugging/Interaction hooks diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 268348eac05..9e6d13fab44 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -486,13 +486,27 @@ def pytest_assertrepr_compare(config, op, left, right): def pytest_assertion_pass(item, lineno, orig, expl): - """Process explanation when assertions are valid. + """ + **(Experimental)** + + Hook called whenever an assertion *passes*. Use this hook to do some processing after a passing assertion. The original assertion information is available in the `orig` string and the pytest introspected assertion information is available in the `expl` string. + This hook must be explicitly enabled by the ``enable_assertion_pass_hook`` + ini-file option: + + .. code-block:: ini + + [pytest] + enable_assertion_pass_hook=true + + You need to **clean the .pyc** files in your project directory and interpreter libraries + when enabling this option, as assertions will require to be re-written. + :param _pytest.nodes.Item item: pytest item object of current test :param int lineno: line number of the assert statement :param string orig: string with original assertion @@ -500,13 +514,10 @@ def pytest_assertion_pass(item, lineno, orig, expl): .. note:: - This hook is still *experimental*, so its parameters or even the hook itself might - be changed/removed without warning in any future pytest release. - - It should be enabled using the `enable_assertion_pass_hook` ini-file option. - Remember to clean the .pyc files in your project directory and interpreter libraries. + This hook is **experimental**, so its parameters or even the hook itself might + be changed/removed without warning in any future pytest release. - If you find this hook useful, please share your feedback opening an issue. + If you find this hook useful, please share your feedback opening an issue. """ From 8edf68f3c0f3f2435fb8b0751dfe82bbd9ecd478 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 19:20:34 -0300 Subject: [PATCH 094/109] Add a trivial note about astor --- changelog/3457.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3457.trivial.rst diff --git a/changelog/3457.trivial.rst b/changelog/3457.trivial.rst new file mode 100644 index 00000000000..f1888763440 --- /dev/null +++ b/changelog/3457.trivial.rst @@ -0,0 +1 @@ +pytest now also depends on the `astor `__ package. From 629eb3ec6a5428ff254646ca568a8cfeb96820c6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 19:26:12 -0300 Subject: [PATCH 095/109] Move formatting variables under the "has impls" if Small optimization, move the generation of the intermediate formatting variables inside the 'if _check_if_assertion_pass_impl():' block. --- src/_pytest/assertion/rewrite.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 7b098ad1b52..8810c156cc1 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -761,11 +761,11 @@ def visit_Assert(self, assert_): ) # If any hooks implement assert_pass hook hook_impl_test = ast.If( - self.helper("_check_if_assertion_pass_impl"), [hook_call_pass], [] + self.helper("_check_if_assertion_pass_impl"), + self.expl_stmts + [hook_call_pass], + [], ) - statements_pass = [] - statements_pass.extend(self.expl_stmts) - statements_pass.append(hook_impl_test) + statements_pass = [hook_impl_test] # Test for assertion condition main_test = ast.If(negation, statements_fail, statements_pass) From 2ea22218ff1070a4afd4f7116b22275c58d090ea Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 20:46:31 -0300 Subject: [PATCH 096/109] Cover assertions with messages when enable_assertion_pass_hook is enabled --- testing/test_assertrewrite.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 5c680927924..8d1c7a5f000 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1363,6 +1363,10 @@ def test_simple(): d=0 assert a+b == c+d + + # cover failing assertions with a message + def test_fails(): + assert False, "assert with message" """ ) result = testdir.runpytest() From 1be49e713ac5187f1bf4715247f9a13f1158ec5f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 26 Jun 2019 20:49:43 -0300 Subject: [PATCH 097/109] Remove py<35 compatibility code from rewrite.py --- src/_pytest/assertion/rewrite.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 8810c156cc1..2a82e9c977c 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -413,9 +413,9 @@ def _check_if_assertion_pass_impl(): return True if util._assertion_pass else False -unary_map = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"} +UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"} -binop_map = { +BINOP_MAP = { ast.BitOr: "|", ast.BitXor: "^", ast.BitAnd: "&", @@ -438,20 +438,8 @@ def _check_if_assertion_pass_impl(): ast.IsNot: "is not", ast.In: "in", ast.NotIn: "not in", + ast.MatMult: "@", } -# Python 3.5+ compatibility -try: - binop_map[ast.MatMult] = "@" -except AttributeError: - pass - -# Python 3.4+ compatibility -if hasattr(ast, "NameConstant"): - _NameConstant = ast.NameConstant -else: - - def _NameConstant(c): - return ast.Name(str(c), ast.Load()) def set_location(node, lineno, col_offset): @@ -774,7 +762,7 @@ def visit_Assert(self, assert_): variables = [ ast.Name(name, ast.Store()) for name in self.format_variables ] - clear_format = ast.Assign(variables, _NameConstant(None)) + clear_format = ast.Assign(variables, ast.NameConstant(None)) self.statements.append(clear_format) else: # Original assertion rewriting @@ -800,7 +788,7 @@ def visit_Assert(self, assert_): # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] - clear = ast.Assign(variables, _NameConstant(None)) + clear = ast.Assign(variables, ast.NameConstant(None)) self.statements.append(clear) # Fix line numbers. for stmt in self.statements: @@ -880,13 +868,13 @@ def visit_BoolOp(self, boolop): return ast.Name(res_var, ast.Load()), self.explanation_param(expl) def visit_UnaryOp(self, unary): - pattern = unary_map[unary.op.__class__] + pattern = UNARY_MAP[unary.op.__class__] operand_res, operand_expl = self.visit(unary.operand) res = self.assign(ast.UnaryOp(unary.op, operand_res)) return res, pattern % (operand_expl,) def visit_BinOp(self, binop): - symbol = binop_map[binop.op.__class__] + symbol = BINOP_MAP[binop.op.__class__] left_expr, left_expl = self.visit(binop.left) right_expr, right_expl = self.visit(binop.right) explanation = "({} {} {})".format(left_expl, symbol, right_expl) @@ -953,7 +941,7 @@ def visit_Compare(self, comp): if isinstance(next_operand, (ast.Compare, ast.BoolOp)): next_expl = "({})".format(next_expl) results.append(next_res) - sym = binop_map[op.__class__] + sym = BINOP_MAP[op.__class__] syms.append(ast.Str(sym)) expl = "{} {} {}".format(left_expl, sym, next_expl) expls.append(ast.Str(expl)) From 3e0e31a3647760f05b4f546a829240686bacef83 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 08:32:32 -0700 Subject: [PATCH 098/109] Don't crash with --pyargs and a filename that looks like a module --- src/_pytest/main.py | 5 ++++- testing/acceptance_test.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 73648557e84..f28bc68db34 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -631,7 +631,10 @@ def _tryconvertpyarg(self, x): """Convert a dotted module name to path.""" try: spec = importlib.util.find_spec(x) - except (ValueError, ImportError): + # AttributeError: looks like package module, but actually filename + # ImportError: module does not exist + # ValueError: not a module name + except (AttributeError, ImportError, ValueError): return x if spec is None or spec.origin in {None, "namespace"}: return x diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index dbdf048a44e..d2a348f40a9 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -646,6 +646,12 @@ def test_pyargs_only_imported_once(self, testdir): # should only configure once assert result.outlines.count("configuring") == 1 + def test_pyargs_filename_looks_like_module(self, testdir): + testdir.tmpdir.join("conftest.py").ensure() + testdir.tmpdir.join("t.py").write("def test(): pass") + result = testdir.runpytest("--pyargs", "t.py") + assert result.ret == ExitCode.OK + def test_cmdline_python_package(self, testdir, monkeypatch): import warnings From 2479a91e929fb36020c1a37ba3f7a593be162802 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Jun 2019 17:53:03 +0200 Subject: [PATCH 099/109] Add Open Collective to FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 1307445ebbd..5f2d1cf09c8 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -2,3 +2,4 @@ # * https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository # * https://tidelift.com/subscription/how-to-connect-tidelift-with-github tidelift: pypi/pytest +open_collective: pytest From 4e723d67508a72878e7afc014d4672944c94cd4f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 10:24:29 -0700 Subject: [PATCH 100/109] Fix crash when discovery fails while using `-p no:terminal` --- changelog/5505.bugfix.rst | 1 + src/_pytest/nodes.py | 2 +- testing/test_config.py | 10 ++++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 changelog/5505.bugfix.rst diff --git a/changelog/5505.bugfix.rst b/changelog/5505.bugfix.rst new file mode 100644 index 00000000000..2d0a53b3925 --- /dev/null +++ b/changelog/5505.bugfix.rst @@ -0,0 +1 @@ +Fix crash when discovery fails while using ``-p no:terminal``. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index f476e414168..491cf9d2c09 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -323,7 +323,7 @@ def repr_failure(self, excinfo): # Respect explicit tbstyle option, but default to "short" # (None._repr_failure_py defaults to "long" without "fulltrace" option). - tbstyle = self.config.getoption("tbstyle") + tbstyle = self.config.getoption("tbstyle", "auto") if tbstyle == "auto": tbstyle = "short" diff --git a/testing/test_config.py b/testing/test_config.py index b9fc388d236..ff993e401c4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -741,10 +741,10 @@ def pytest_addoption(parser): **{ "conftest": conftest_source, "subdir/conftest": conftest_source, - "subdir/test_foo": """ + "subdir/test_foo": """\ def test_foo(pytestconfig): assert pytestconfig.getini('foo') == 'subdir' - """, + """, } ) @@ -777,6 +777,12 @@ def pytest_internalerror(self, excrepr): assert "ValueError" in err +def test_no_terminal_discovery_error(testdir): + testdir.makepyfile("raise TypeError('oops!')") + result = testdir.runpytest("-p", "no:terminal", "--collect-only") + assert result.ret == ExitCode.INTERRUPTED + + def test_load_initial_conftest_last_ordering(testdir, _config_for_test): pm = _config_for_test.pluginmanager From 45af361a677c7a03369e59959688a1d03c99c61a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 10:45:37 -0700 Subject: [PATCH 101/109] Remove stray comment from tox.ini --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 82bad1063df..9757b098367 100644 --- a/tox.ini +++ b/tox.ini @@ -96,7 +96,6 @@ commands = [testenv:py37-freeze] changedir = testing/freeze -# Disable PEP 517 with pip, which does not work with PyInstaller currently. deps = pyinstaller commands = From bf39e89946c1c30fe12967a8dd7867576d8a2c36 Mon Sep 17 00:00:00 2001 From: AmirElkess Date: Fri, 28 Jun 2019 21:16:17 +0200 Subject: [PATCH 102/109] Refactoring doctests --- src/_pytest/doctest.py | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 50c81902684..ca6e4675f5a 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -364,30 +364,27 @@ def _patch_unwrap_mock_aware(): contextmanager which replaces ``inspect.unwrap`` with a version that's aware of mock objects and doesn't recurse on them """ - real_unwrap = getattr(inspect, "unwrap", None) - if real_unwrap is None: - yield - else: - - def _mock_aware_unwrap(obj, stop=None): - try: - if stop is None or stop is _is_mocked: - return real_unwrap(obj, stop=_is_mocked) - return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj)) - except Exception as e: - warnings.warn( - "Got %r when unwrapping %r. This is usually caused " - "by a violation of Python's object protocol; see e.g. " - "https://github.com/pytest-dev/pytest/issues/5080" % (e, obj), - PytestWarning, - ) - raise + real_unwrap = inspect.unwrap - inspect.unwrap = _mock_aware_unwrap + def _mock_aware_unwrap(obj, stop=None): try: - yield - finally: - inspect.unwrap = real_unwrap + if stop is None or stop is _is_mocked: + return real_unwrap(obj, stop=_is_mocked) + return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj)) + except Exception as e: + warnings.warn( + "Got %r when unwrapping %r. This is usually caused " + "by a violation of Python's object protocol; see e.g. " + "https://github.com/pytest-dev/pytest/issues/5080" % (e, obj), + PytestWarning, + ) + raise + + inspect.unwrap = _mock_aware_unwrap + try: + yield + finally: + inspect.unwrap = real_unwrap class DoctestModule(pytest.Module): From 7ee244476a4ecc2b39f9bf21afea8ebf57077941 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 19:11:20 -0700 Subject: [PATCH 103/109] Remove astor and reproduce the original assertion expression --- changelog/3457.trivial.rst | 1 - setup.py | 1 - src/_pytest/assertion/rewrite.py | 72 +++++++++++-- testing/test_assertrewrite.py | 178 +++++++++++++++++++++++-------- 4 files changed, 197 insertions(+), 55 deletions(-) delete mode 100644 changelog/3457.trivial.rst diff --git a/changelog/3457.trivial.rst b/changelog/3457.trivial.rst deleted file mode 100644 index f1888763440..00000000000 --- a/changelog/3457.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -pytest now also depends on the `astor `__ package. diff --git a/setup.py b/setup.py index 7d953281611..4c87c6429bb 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ "pluggy>=0.12,<1.0", "importlib-metadata>=0.12", "wcwidth", - "astor", ] diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 2a82e9c977c..8b2c1e14610 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1,16 +1,18 @@ """Rewrite assertion AST to produce nice error messages""" import ast import errno +import functools import importlib.machinery import importlib.util +import io import itertools import marshal import os import struct import sys +import tokenize import types -import astor import atomicwrites from _pytest._io.saferepr import saferepr @@ -285,7 +287,7 @@ def _rewrite_test(fn, config): with open(fn, "rb") as f: source = f.read() tree = ast.parse(source, filename=fn) - rewrite_asserts(tree, fn, config) + rewrite_asserts(tree, source, fn, config) co = compile(tree, fn, "exec", dont_inherit=True) return stat, co @@ -327,9 +329,9 @@ def _read_pyc(source, pyc, trace=lambda x: None): return co -def rewrite_asserts(mod, module_path=None, config=None): +def rewrite_asserts(mod, source, module_path=None, config=None): """Rewrite the assert statements in mod.""" - AssertionRewriter(module_path, config).run(mod) + AssertionRewriter(module_path, config, source).run(mod) def _saferepr(obj): @@ -457,6 +459,59 @@ def _fix(node, lineno, col_offset): return node +def _get_assertion_exprs(src: bytes): # -> Dict[int, str] + """Returns a mapping from {lineno: "assertion test expression"}""" + ret = {} + + depth = 0 + lines = [] + assert_lineno = None + seen_lines = set() + + def _write_and_reset() -> None: + nonlocal depth, lines, assert_lineno, seen_lines + ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\") + depth = 0 + lines = [] + assert_lineno = None + seen_lines = set() + + tokens = tokenize.tokenize(io.BytesIO(src).readline) + for tp, src, (lineno, offset), _, line in tokens: + if tp == tokenize.NAME and src == "assert": + assert_lineno = lineno + elif assert_lineno is not None: + # keep track of depth for the assert-message `,` lookup + if tp == tokenize.OP and src in "([{": + depth += 1 + elif tp == tokenize.OP and src in ")]}": + depth -= 1 + + if not lines: + lines.append(line[offset:]) + seen_lines.add(lineno) + # a non-nested comma separates the expression from the message + elif depth == 0 and tp == tokenize.OP and src == ",": + # one line assert with message + if lineno in seen_lines and len(lines) == 1: + offset_in_trimmed = offset + len(lines[-1]) - len(line) + lines[-1] = lines[-1][:offset_in_trimmed] + # multi-line assert with message + elif lineno in seen_lines: + lines[-1] = lines[-1][:offset] + # multi line assert with escapd newline before message + else: + lines.append(line[:offset]) + _write_and_reset() + elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}: + _write_and_reset() + elif lines and lineno not in seen_lines: + lines.append(line) + seen_lines.add(lineno) + + return ret + + class AssertionRewriter(ast.NodeVisitor): """Assertion rewriting implementation. @@ -511,7 +566,7 @@ class AssertionRewriter(ast.NodeVisitor): """ - def __init__(self, module_path, config): + def __init__(self, module_path, config, source): super().__init__() self.module_path = module_path self.config = config @@ -521,6 +576,11 @@ def __init__(self, module_path, config): ) else: self.enable_assertion_pass_hook = False + self.source = source + + @functools.lru_cache(maxsize=1) + def _assert_expr_to_lineno(self): + return _get_assertion_exprs(self.source) def run(self, mod): """Find all assert statements in *mod* and rewrite them.""" @@ -738,7 +798,7 @@ def visit_Assert(self, assert_): # Passed fmt_pass = self.helper("_format_explanation", msg) - orig = astor.to_source(assert_.test).rstrip("\n").lstrip("(").rstrip(")") + orig = self._assert_expr_to_lineno()[assert_.lineno] hook_call_pass = ast.Expr( self.helper( "_call_assertion_pass", diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8d1c7a5f000..b8242b37d1c 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -13,6 +13,7 @@ import _pytest._code import pytest from _pytest.assertion import util +from _pytest.assertion.rewrite import _get_assertion_exprs from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts @@ -31,7 +32,7 @@ def teardown_module(mod): def rewrite(src): tree = ast.parse(src) - rewrite_asserts(tree) + rewrite_asserts(tree, src.encode()) return tree @@ -1292,10 +1293,10 @@ def test_pattern_contains_subdirectories(self, testdir, hook): """ p = testdir.makepyfile( **{ - "tests/file.py": """ - def test_simple_failure(): - assert 1 + 1 == 3 - """ + "tests/file.py": """\ + def test_simple_failure(): + assert 1 + 1 == 3 + """ } ) testdir.syspathinsert(p.dirpath()) @@ -1315,19 +1316,19 @@ def test_cwd_changed(self, testdir, monkeypatch): testdir.makepyfile( **{ - "test_setup_nonexisting_cwd.py": """ - import os - import shutil - import tempfile - - d = tempfile.mkdtemp() - os.chdir(d) - shutil.rmtree(d) - """, - "test_test.py": """ - def test(): - pass - """, + "test_setup_nonexisting_cwd.py": """\ + import os + import shutil + import tempfile + + d = tempfile.mkdtemp() + os.chdir(d) + shutil.rmtree(d) + """, + "test_test.py": """\ + def test(): + pass + """, } ) result = testdir.runpytest() @@ -1339,23 +1340,22 @@ def test_option_default(self, testdir): config = testdir.parseconfig() assert config.getini("enable_assertion_pass_hook") is False - def test_hook_call(self, testdir): + @pytest.fixture + def flag_on(self, testdir): + testdir.makeini("[pytest]\nenable_assertion_pass_hook = True\n") + + @pytest.fixture + def hook_on(self, testdir): testdir.makeconftest( - """ + """\ def pytest_assertion_pass(item, lineno, orig, expl): raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno)) """ ) - testdir.makeini( - """ - [pytest] - enable_assertion_pass_hook = True - """ - ) - + def test_hook_call(self, testdir, flag_on, hook_on): testdir.makepyfile( - """ + """\ def test_simple(): a=1 b=2 @@ -1371,10 +1371,21 @@ def test_fails(): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - "*Assertion Passed: a + b == c + d (1 + 2) == (3 + 0) at line 7*" + "*Assertion Passed: a+b == c+d (1 + 2) == (3 + 0) at line 7*" + ) + + def test_hook_call_with_parens(self, testdir, flag_on, hook_on): + testdir.makepyfile( + """\ + def f(): return 1 + def test(): + assert f() + """ ) + result = testdir.runpytest() + result.stdout.fnmatch_lines("*Assertion Passed: f() 1") - def test_hook_not_called_without_hookimpl(self, testdir, monkeypatch): + def test_hook_not_called_without_hookimpl(self, testdir, monkeypatch, flag_on): """Assertion pass should not be called (and hence formatting should not occur) if there is no hook declared for pytest_assertion_pass""" @@ -1385,15 +1396,8 @@ def raise_on_assertionpass(*_, **__): _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass ) - testdir.makeini( - """ - [pytest] - enable_assertion_pass_hook = True - """ - ) - testdir.makepyfile( - """ + """\ def test_simple(): a=1 b=2 @@ -1418,21 +1422,14 @@ def raise_on_assertionpass(*_, **__): ) testdir.makeconftest( - """ + """\ def pytest_assertion_pass(item, lineno, orig, expl): raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno)) """ ) - testdir.makeini( - """ - [pytest] - enable_assertion_pass_hook = False - """ - ) - testdir.makepyfile( - """ + """\ def test_simple(): a=1 b=2 @@ -1444,3 +1441,90 @@ def test_simple(): ) result = testdir.runpytest() result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize( + ("src", "expected"), + ( + # fmt: off + pytest.param(b"", {}, id="trivial"), + pytest.param( + b"def x(): assert 1\n", + {1: "1"}, + id="assert statement not on own line", + ), + pytest.param( + b"def x():\n" + b" assert 1\n" + b" assert 1+2\n", + {2: "1", 3: "1+2"}, + id="multiple assertions", + ), + pytest.param( + # changes in encoding cause the byte offsets to be different + "# -*- coding: latin1\n" + "def ÀÀÀÀÀ(): assert 1\n".encode("latin1"), + {2: "1"}, + id="latin1 encoded on first line\n", + ), + pytest.param( + # using the default utf-8 encoding + "def ÀÀÀÀÀ(): assert 1\n".encode(), + {1: "1"}, + id="utf-8 encoded on first line", + ), + pytest.param( + b"def x():\n" + b" assert (\n" + b" 1 + 2 # comment\n" + b" )\n", + {2: "(\n 1 + 2 # comment\n )"}, + id="multi-line assertion", + ), + pytest.param( + b"def x():\n" + b" assert y == [\n" + b" 1, 2, 3\n" + b" ]\n", + {2: "y == [\n 1, 2, 3\n ]"}, + id="multi line assert with list continuation", + ), + pytest.param( + b"def x():\n" + b" assert 1 + \\\n" + b" 2\n", + {2: "1 + \\\n 2"}, + id="backslash continuation", + ), + pytest.param( + b"def x():\n" + b" assert x, y\n", + {2: "x"}, + id="assertion with message", + ), + pytest.param( + b"def x():\n" + b" assert (\n" + b" f(1, 2, 3)\n" + b" ), 'f did not work!'\n", + {2: "(\n f(1, 2, 3)\n )"}, + id="assertion with message, test spanning multiple lines", + ), + pytest.param( + b"def x():\n" + b" assert \\\n" + b" x\\\n" + b" , 'failure message'\n", + {2: "x"}, + id="escaped newlines plus message", + ), + pytest.param( + b"def x(): assert 5", + {1: "5"}, + id="no newline at end of file", + ), + # fmt: on + ), +) +def test_get_assertion_exprs(src, expected): + assert _get_assertion_exprs(src) == expected From fd2f32048554da04d60bae662a82ecd0fc8417e2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 28 Jun 2019 14:39:53 -0700 Subject: [PATCH 104/109] Preparing release version 5.0.0 --- CHANGELOG.rst | 178 ++++++++++++++++++++++++++++++ changelog/1149.removal.rst | 7 -- changelog/1403.bugfix.rst | 1 - changelog/1671.bugfix.rst | 2 - changelog/2761.bugfix.rst | 1 - changelog/3457.feature.rst | 4 - changelog/4488.deprecation.rst | 2 - changelog/466.deprecation.rst | 2 - changelog/5078.bugfix.rst | 1 - changelog/5125.removal.rst | 5 - changelog/5260.bugfix.rst | 17 --- changelog/5315.doc.rst | 1 - changelog/5335.bugfix.rst | 2 - changelog/5354.bugfix.rst | 1 - changelog/5370.bugfix.rst | 1 - changelog/5371.bugfix.rst | 1 - changelog/5372.bugfix.rst | 1 - changelog/5383.bugfix.rst | 2 - changelog/5389.bugfix.rst | 1 - changelog/5390.bugfix.rst | 1 - changelog/5402.removal.rst | 23 ---- changelog/5404.bugfix.rst | 2 - changelog/5412.removal.rst | 2 - changelog/5416.doc.rst | 1 - changelog/5432.bugfix.rst | 1 - changelog/5433.bugfix.rst | 1 - changelog/5440.feature.rst | 8 -- changelog/5444.bugfix.rst | 1 - changelog/5452.feature.rst | 1 - changelog/5482.bugfix.rst | 2 - changelog/5505.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-5.0.0.rst | 47 ++++++++ doc/en/example/parametrize.rst | 12 +- doc/en/example/reportingdemo.rst | 126 +++++++++++---------- 35 files changed, 301 insertions(+), 159 deletions(-) delete mode 100644 changelog/1149.removal.rst delete mode 100644 changelog/1403.bugfix.rst delete mode 100644 changelog/1671.bugfix.rst delete mode 100644 changelog/2761.bugfix.rst delete mode 100644 changelog/3457.feature.rst delete mode 100644 changelog/4488.deprecation.rst delete mode 100644 changelog/466.deprecation.rst delete mode 100644 changelog/5078.bugfix.rst delete mode 100644 changelog/5125.removal.rst delete mode 100644 changelog/5260.bugfix.rst delete mode 100644 changelog/5315.doc.rst delete mode 100644 changelog/5335.bugfix.rst delete mode 100644 changelog/5354.bugfix.rst delete mode 100644 changelog/5370.bugfix.rst delete mode 100644 changelog/5371.bugfix.rst delete mode 100644 changelog/5372.bugfix.rst delete mode 100644 changelog/5383.bugfix.rst delete mode 100644 changelog/5389.bugfix.rst delete mode 100644 changelog/5390.bugfix.rst delete mode 100644 changelog/5402.removal.rst delete mode 100644 changelog/5404.bugfix.rst delete mode 100644 changelog/5412.removal.rst delete mode 100644 changelog/5416.doc.rst delete mode 100644 changelog/5432.bugfix.rst delete mode 100644 changelog/5433.bugfix.rst delete mode 100644 changelog/5440.feature.rst delete mode 100644 changelog/5444.bugfix.rst delete mode 100644 changelog/5452.feature.rst delete mode 100644 changelog/5482.bugfix.rst delete mode 100644 changelog/5505.bugfix.rst create mode 100644 doc/en/announce/release-5.0.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45e31bef94b..27d15a8754a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,184 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.0.0 (2019-06-28) +========================= + +Removals +-------- + +- `#1149 `_: Pytest no longer accepts prefixes of command-line arguments, for example + typing ``pytest --doctest-mod`` inplace of ``--doctest-modules``. + This was previously allowed where the ``ArgumentParser`` thought it was unambiguous, + but this could be incorrect due to delayed parsing of options for plugins. + See for example issues `#1149 `__, + `#3413 `__, and + `#4009 `__. + + +- `#5125 `_: ``Session.exitcode`` values are now coded in ``pytest.ExitCode``, an ``IntEnum``. This makes the exit code available for consumer code and are more explicit other than just documentation. User defined exit codes are still valid, but should be used with caution. + + The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios. + + **pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change. + + +- `#5402 `_: **PytestDeprecationWarning are now errors by default.** + + Following our plan to remove deprecated features with as little disruption as + possible, all warnings of type ``PytestDeprecationWarning`` now generate errors + instead of warning messages. + + **The affected features will be effectively removed in pytest 5.1**, so please consult the + `Deprecations and Removals `__ + section in the docs for directions on how to update existing code. + + In the pytest ``5.0.X`` series, it is possible to change the errors back into warnings as a stop + gap measure by adding this to your ``pytest.ini`` file: + + .. code-block:: ini + + [pytest] + filterwarnings = + ignore::pytest.PytestDeprecationWarning + + But this will stop working when pytest ``5.1`` is released. + + **If you have concerns** about the removal of a specific feature, please add a + comment to `#5402 `__. + + +- `#5412 `_: ``ExceptionInfo`` objects (returned by ``pytest.raises``) now have the same ``str`` representation as ``repr``, which + avoids some confusion when users use ``print(e)`` to inspect the object. + + + +Deprecations +------------ + +- `#4488 `_: The removal of the ``--result-log`` option and module has been postponed to (tentatively) pytest 6.0 as + the team has not yet got around to implement a good alternative for it. + + +- `#466 `_: The ``funcargnames`` attribute has been an alias for ``fixturenames`` since + pytest 2.3, and is now deprecated in code too. + + + +Features +-------- + +- `#3457 `_: New `pytest_assertion_pass `__ + hook, called with context information when an assertion *passes*. + + This hook is still **experimental** so use it with caution. + + +- `#5440 `_: The `faulthandler `__ standard library + module is now enabled by default to help users diagnose crashes in C modules. + + This functionality was provided by integrating the external + `pytest-faulthandler `__ plugin into the core, + so users should remove that plugin from their requirements if used. + + For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler + + +- `#5452 `_: When warnings are configured as errors, pytest warnings now appear as originating from ``pytest.`` instead of the internal ``_pytest.warning_types.`` module. + + + +Bug Fixes +--------- + +- `#1403 `_: Switch from ``imp`` to ``importlib``. + + +- `#1671 `_: The name of the ``.pyc`` files cached by the assertion writer now includes the pytest version + to avoid stale caches. + + +- `#2761 `_: Honor PEP 235 on case-insensitive file systems. + + +- `#5078 `_: Test module is no longer double-imported when using ``--pyargs``. + + +- `#5260 `_: Improved comparison of byte strings. + + When comparing bytes, the assertion message used to show the byte numeric value when showing the differences:: + + def test(): + > assert b'spam' == b'eggs' + E AssertionError: assert b'spam' == b'eggs' + E At index 0 diff: 115 != 101 + E Use -v to get the full diff + + It now shows the actual ascii representation instead, which is often more useful:: + + def test(): + > assert b'spam' == b'eggs' + E AssertionError: assert b'spam' == b'eggs' + E At index 0 diff: b's' != b'e' + E Use -v to get the full diff + + +- `#5335 `_: Colorize level names when the level in the logging format is formatted using + '%(levelname).Xs' (truncated fixed width alignment), where X is an integer. + + +- `#5354 `_: Fix ``pytest.mark.parametrize`` when the argvalues is an iterator. + + +- `#5370 `_: Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions. + + +- `#5371 `_: Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``. + + +- `#5372 `_: Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression. + + +- `#5383 `_: ``-q`` has again an impact on the style of the collected items + (``--collect-only``) when ``--log-cli-level`` is used. + + +- `#5389 `_: Fix regressions of `#5063 `__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. + + +- `#5390 `_: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. + + +- `#5404 `_: Emit a warning when attempting to unwrap a broken object raises an exception, + for easier debugging (`#5080 `__). + + +- `#5432 `_: Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times. + + +- `#5433 `_: Fix assertion rewriting in packages (``__init__.py``). + + +- `#5444 `_: Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect. + + +- `#5482 `_: Fix bug introduced in 4.6.0 causing collection errors when passing + more than 2 positional arguments to ``pytest.mark.parametrize``. + + +- `#5505 `_: Fix crash when discovery fails while using ``-p no:terminal``. + + + +Improved Documentation +---------------------- + +- `#5315 `_: Expand docs on mocking classes and dictionaries with ``monkeypatch``. + + +- `#5416 `_: Fix PytestUnknownMarkWarning in run/skip example. + + pytest 4.6.3 (2019-06-11) ========================= diff --git a/changelog/1149.removal.rst b/changelog/1149.removal.rst deleted file mode 100644 index f507014d92b..00000000000 --- a/changelog/1149.removal.rst +++ /dev/null @@ -1,7 +0,0 @@ -Pytest no longer accepts prefixes of command-line arguments, for example -typing ``pytest --doctest-mod`` inplace of ``--doctest-modules``. -This was previously allowed where the ``ArgumentParser`` thought it was unambiguous, -but this could be incorrect due to delayed parsing of options for plugins. -See for example issues `#1149 `__, -`#3413 `__, and -`#4009 `__. diff --git a/changelog/1403.bugfix.rst b/changelog/1403.bugfix.rst deleted file mode 100644 index 3fb748aec59..00000000000 --- a/changelog/1403.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Switch from ``imp`` to ``importlib``. diff --git a/changelog/1671.bugfix.rst b/changelog/1671.bugfix.rst deleted file mode 100644 index c46eac82866..00000000000 --- a/changelog/1671.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -The name of the ``.pyc`` files cached by the assertion writer now includes the pytest version -to avoid stale caches. diff --git a/changelog/2761.bugfix.rst b/changelog/2761.bugfix.rst deleted file mode 100644 index c63f02ecd58..00000000000 --- a/changelog/2761.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Honor PEP 235 on case-insensitive file systems. diff --git a/changelog/3457.feature.rst b/changelog/3457.feature.rst deleted file mode 100644 index c309430706c..00000000000 --- a/changelog/3457.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -New `pytest_assertion_pass `__ -hook, called with context information when an assertion *passes*. - -This hook is still **experimental** so use it with caution. diff --git a/changelog/4488.deprecation.rst b/changelog/4488.deprecation.rst deleted file mode 100644 index 575df554539..00000000000 --- a/changelog/4488.deprecation.rst +++ /dev/null @@ -1,2 +0,0 @@ -The removal of the ``--result-log`` option and module has been postponed to (tentatively) pytest 6.0 as -the team has not yet got around to implement a good alternative for it. diff --git a/changelog/466.deprecation.rst b/changelog/466.deprecation.rst deleted file mode 100644 index 65775c3862d..00000000000 --- a/changelog/466.deprecation.rst +++ /dev/null @@ -1,2 +0,0 @@ -The ``funcargnames`` attribute has been an alias for ``fixturenames`` since -pytest 2.3, and is now deprecated in code too. diff --git a/changelog/5078.bugfix.rst b/changelog/5078.bugfix.rst deleted file mode 100644 index 8fed85f5da9..00000000000 --- a/changelog/5078.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Test module is no longer double-imported when using ``--pyargs``. diff --git a/changelog/5125.removal.rst b/changelog/5125.removal.rst deleted file mode 100644 index 8f67c7399ae..00000000000 --- a/changelog/5125.removal.rst +++ /dev/null @@ -1,5 +0,0 @@ -``Session.exitcode`` values are now coded in ``pytest.ExitCode``, an ``IntEnum``. This makes the exit code available for consumer code and are more explicit other than just documentation. User defined exit codes are still valid, but should be used with caution. - -The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios. - -**pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change. diff --git a/changelog/5260.bugfix.rst b/changelog/5260.bugfix.rst deleted file mode 100644 index 484c1438ae6..00000000000 --- a/changelog/5260.bugfix.rst +++ /dev/null @@ -1,17 +0,0 @@ -Improved comparison of byte strings. - -When comparing bytes, the assertion message used to show the byte numeric value when showing the differences:: - - def test(): - > assert b'spam' == b'eggs' - E AssertionError: assert b'spam' == b'eggs' - E At index 0 diff: 115 != 101 - E Use -v to get the full diff - -It now shows the actual ascii representation instead, which is often more useful:: - - def test(): - > assert b'spam' == b'eggs' - E AssertionError: assert b'spam' == b'eggs' - E At index 0 diff: b's' != b'e' - E Use -v to get the full diff diff --git a/changelog/5315.doc.rst b/changelog/5315.doc.rst deleted file mode 100644 index 4cb46358308..00000000000 --- a/changelog/5315.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Expand docs on mocking classes and dictionaries with ``monkeypatch``. diff --git a/changelog/5335.bugfix.rst b/changelog/5335.bugfix.rst deleted file mode 100644 index 0a2e99dc9f1..00000000000 --- a/changelog/5335.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Colorize level names when the level in the logging format is formatted using -'%(levelname).Xs' (truncated fixed width alignment), where X is an integer. diff --git a/changelog/5354.bugfix.rst b/changelog/5354.bugfix.rst deleted file mode 100644 index 812ea8364aa..00000000000 --- a/changelog/5354.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``pytest.mark.parametrize`` when the argvalues is an iterator. diff --git a/changelog/5370.bugfix.rst b/changelog/5370.bugfix.rst deleted file mode 100644 index 70def0d270a..00000000000 --- a/changelog/5370.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions. diff --git a/changelog/5371.bugfix.rst b/changelog/5371.bugfix.rst deleted file mode 100644 index 46ff5c89047..00000000000 --- a/changelog/5371.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``. diff --git a/changelog/5372.bugfix.rst b/changelog/5372.bugfix.rst deleted file mode 100644 index e9b644db290..00000000000 --- a/changelog/5372.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression. diff --git a/changelog/5383.bugfix.rst b/changelog/5383.bugfix.rst deleted file mode 100644 index 53e25956d56..00000000000 --- a/changelog/5383.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``-q`` has again an impact on the style of the collected items -(``--collect-only``) when ``--log-cli-level`` is used. diff --git a/changelog/5389.bugfix.rst b/changelog/5389.bugfix.rst deleted file mode 100644 index debf0a9da9d..00000000000 --- a/changelog/5389.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regressions of `#5063 `__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. diff --git a/changelog/5390.bugfix.rst b/changelog/5390.bugfix.rst deleted file mode 100644 index 3f57c3043d5..00000000000 --- a/changelog/5390.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. diff --git a/changelog/5402.removal.rst b/changelog/5402.removal.rst deleted file mode 100644 index 29921dd9763..00000000000 --- a/changelog/5402.removal.rst +++ /dev/null @@ -1,23 +0,0 @@ -**PytestDeprecationWarning are now errors by default.** - -Following our plan to remove deprecated features with as little disruption as -possible, all warnings of type ``PytestDeprecationWarning`` now generate errors -instead of warning messages. - -**The affected features will be effectively removed in pytest 5.1**, so please consult the -`Deprecations and Removals `__ -section in the docs for directions on how to update existing code. - -In the pytest ``5.0.X`` series, it is possible to change the errors back into warnings as a stop -gap measure by adding this to your ``pytest.ini`` file: - -.. code-block:: ini - - [pytest] - filterwarnings = - ignore::pytest.PytestDeprecationWarning - -But this will stop working when pytest ``5.1`` is released. - -**If you have concerns** about the removal of a specific feature, please add a -comment to `#5402 `__. diff --git a/changelog/5404.bugfix.rst b/changelog/5404.bugfix.rst deleted file mode 100644 index 2187bed8b32..00000000000 --- a/changelog/5404.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Emit a warning when attempting to unwrap a broken object raises an exception, -for easier debugging (`#5080 `__). diff --git a/changelog/5412.removal.rst b/changelog/5412.removal.rst deleted file mode 100644 index a6f19700629..00000000000 --- a/changelog/5412.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -``ExceptionInfo`` objects (returned by ``pytest.raises``) now have the same ``str`` representation as ``repr``, which -avoids some confusion when users use ``print(e)`` to inspect the object. diff --git a/changelog/5416.doc.rst b/changelog/5416.doc.rst deleted file mode 100644 index 81e4c640441..00000000000 --- a/changelog/5416.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Fix PytestUnknownMarkWarning in run/skip example. diff --git a/changelog/5432.bugfix.rst b/changelog/5432.bugfix.rst deleted file mode 100644 index 44c01c0cf59..00000000000 --- a/changelog/5432.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times. diff --git a/changelog/5433.bugfix.rst b/changelog/5433.bugfix.rst deleted file mode 100644 index c3a7472bc6c..00000000000 --- a/changelog/5433.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix assertion rewriting in packages (``__init__.py``). diff --git a/changelog/5440.feature.rst b/changelog/5440.feature.rst deleted file mode 100644 index d3bb95f5841..00000000000 --- a/changelog/5440.feature.rst +++ /dev/null @@ -1,8 +0,0 @@ -The `faulthandler `__ standard library -module is now enabled by default to help users diagnose crashes in C modules. - -This functionality was provided by integrating the external -`pytest-faulthandler `__ plugin into the core, -so users should remove that plugin from their requirements if used. - -For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler diff --git a/changelog/5444.bugfix.rst b/changelog/5444.bugfix.rst deleted file mode 100644 index 230d4b49eb6..00000000000 --- a/changelog/5444.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect. diff --git a/changelog/5452.feature.rst b/changelog/5452.feature.rst deleted file mode 100644 index 4e47e971ea7..00000000000 --- a/changelog/5452.feature.rst +++ /dev/null @@ -1 +0,0 @@ -When warnings are configured as errors, pytest warnings now appear as originating from ``pytest.`` instead of the internal ``_pytest.warning_types.`` module. diff --git a/changelog/5482.bugfix.rst b/changelog/5482.bugfix.rst deleted file mode 100644 index c345458d18e..00000000000 --- a/changelog/5482.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix bug introduced in 4.6.0 causing collection errors when passing -more than 2 positional arguments to ``pytest.mark.parametrize``. diff --git a/changelog/5505.bugfix.rst b/changelog/5505.bugfix.rst deleted file mode 100644 index 2d0a53b3925..00000000000 --- a/changelog/5505.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix crash when discovery fails while using ``-p no:terminal``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index c8c7f243a4b..c8247ceb37b 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.0.0 release-4.6.3 release-4.6.2 release-4.6.1 diff --git a/doc/en/announce/release-5.0.0.rst b/doc/en/announce/release-5.0.0.rst new file mode 100644 index 00000000000..a198c6fa6dc --- /dev/null +++ b/doc/en/announce/release-5.0.0.rst @@ -0,0 +1,47 @@ +pytest-5.0.0 +======================================= + +The pytest team is proud to announce the 5.0.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Dirk Thomas +* Evan Kepner +* Florian Bruhin +* Hugo +* Kevin J. Foley +* Pulkit Goyal +* Ralph Giles +* Ronny Pfannschmidt +* Thomas Grainger +* Thomas Hisch +* Tim Gates +* Victor Maryama +* Yuri Apollov +* Zac Hatfield-Dodds +* Zac-HD +* curiousjazz77 +* patriksevallius + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index d324fc106cb..8c0e4754f6d 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -434,10 +434,16 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ...sss...sssssssss...sss... [100%] + ssssssssssss......sss...... [100%] + ============================= warnings summary ============================= + $PYTHON_PREFIX/lib/python3.6/distutils/__init__.py:1 + $PYTHON_PREFIX/lib/python3.6/distutils/__init__.py:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses + import imp + + -- Docs: https://docs.pytest.org/en/latest/warnings.html ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found - 12 passed, 15 skipped in 0.12 seconds + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found + 12 passed, 15 skipped, 1 warnings in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 928c365cafc..617681c1980 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -26,7 +26,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert param1 * 2 < param2 E assert (3 * 2) < 6 - failure_demo.py:21: AssertionError + failure_demo.py:20: AssertionError _________________________ TestFailing.test_simple __________________________ self = @@ -43,7 +43,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 42 = .f at 0xdeadbeef>() E + and 43 = .g at 0xdeadbeef>() - failure_demo.py:32: AssertionError + failure_demo.py:31: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ self = @@ -51,7 +51,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_simple_multiline(self): > otherfunc_multi(42, 6 * 9) - failure_demo.py:35: + failure_demo.py:34: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 42, b = 54 @@ -60,7 +60,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 42 == 54 - failure_demo.py:16: AssertionError + failure_demo.py:15: AssertionError ___________________________ TestFailing.test_not ___________________________ self = @@ -73,7 +73,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert not 42 E + where 42 = .f at 0xdeadbeef>() - failure_demo.py:41: AssertionError + failure_demo.py:40: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ self = @@ -84,7 +84,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - spam E + eggs - failure_demo.py:46: AssertionError + failure_demo.py:45: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ self = @@ -97,7 +97,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + foo 2 bar E ? ^ - failure_demo.py:49: AssertionError + failure_demo.py:48: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ self = @@ -110,7 +110,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + eggs E bar - failure_demo.py:52: AssertionError + failure_demo.py:51: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ self = @@ -127,7 +127,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + 1111111111b222222222 E ? ^ - failure_demo.py:57: AssertionError + failure_demo.py:56: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ self = @@ -147,7 +147,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (7 lines hidden), use '-vv' to show - failure_demo.py:62: AssertionError + failure_demo.py:61: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ self = @@ -158,7 +158,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 2 diff: 2 != 3 E Use -v to get the full diff - failure_demo.py:65: AssertionError + failure_demo.py:64: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ self = @@ -171,7 +171,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 100 diff: 1 != 2 E Use -v to get the full diff - failure_demo.py:70: AssertionError + failure_demo.py:69: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ self = @@ -189,7 +189,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:73: AssertionError + failure_demo.py:72: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ self = @@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:76: AssertionError + failure_demo.py:75: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ self = @@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Right contains one more item: 3 E Use -v to get the full diff - failure_demo.py:79: AssertionError + failure_demo.py:78: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ self = @@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5] - failure_demo.py:82: AssertionError + failure_demo.py:81: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ self = @@ -246,7 +246,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:86: AssertionError + failure_demo.py:85: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ self = @@ -259,7 +259,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E single foo line E ? +++ - failure_demo.py:90: AssertionError + failure_demo.py:89: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ self = @@ -272,7 +272,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ - failure_demo.py:94: AssertionError + failure_demo.py:93: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ self = @@ -285,7 +285,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - failure_demo.py:98: AssertionError + failure_demo.py:97: AssertionError ______________ TestSpecialisedExplanations.test_eq_dataclass _______________ self = @@ -294,7 +294,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: from dataclasses import dataclass @dataclass - class Foo(object): + class Foo: a: int b: str @@ -306,7 +306,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Differing attributes: E b: 'b' != 'c' - failure_demo.py:110: AssertionError + failure_demo.py:109: AssertionError ________________ TestSpecialisedExplanations.test_eq_attrs _________________ self = @@ -315,7 +315,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: import attr @attr.s - class Foo(object): + class Foo: a = attr.ib() b = attr.ib() @@ -327,11 +327,11 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Differing attributes: E b: 'b' != 'c' - failure_demo.py:122: AssertionError + failure_demo.py:121: AssertionError ______________________________ test_attribute ______________________________ def test_attribute(): - class Foo(object): + class Foo: b = 1 i = Foo() @@ -339,11 +339,11 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .Foo object at 0xdeadbeef>.b - failure_demo.py:130: AssertionError + failure_demo.py:129: AssertionError _________________________ test_attribute_instance __________________________ def test_attribute_instance(): - class Foo(object): + class Foo: b = 1 > assert Foo().b == 2 @@ -351,11 +351,11 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = .Foo object at 0xdeadbeef>.b E + where .Foo object at 0xdeadbeef> = .Foo'>() - failure_demo.py:137: AssertionError + failure_demo.py:136: AssertionError __________________________ test_attribute_failure __________________________ def test_attribute_failure(): - class Foo(object): + class Foo: def _get_b(self): raise Exception("Failed to get attrib") @@ -364,7 +364,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: i = Foo() > assert i.b == 2 - failure_demo.py:148: + failure_demo.py:147: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = .Foo object at 0xdeadbeef> @@ -373,14 +373,14 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise Exception("Failed to get attrib") E Exception: Failed to get attrib - failure_demo.py:143: Exception + failure_demo.py:142: Exception _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): - class Foo(object): + class Foo: b = 1 - class Bar(object): + class Bar: b = 2 > assert Foo().b == Bar().b @@ -390,7 +390,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + and 2 = .Bar object at 0xdeadbeef>.b E + where .Bar object at 0xdeadbeef> = .Bar'>() - failure_demo.py:158: AssertionError + failure_demo.py:157: AssertionError __________________________ TestRaises.test_raises __________________________ self = @@ -400,7 +400,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(TypeError, int, s) E ValueError: invalid literal for int() with base 10: 'qwe' - failure_demo.py:168: ValueError + failure_demo.py:167: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ self = @@ -409,7 +409,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(IOError, int, "3") E Failed: DID NOT RAISE - failure_demo.py:171: Failed + failure_demo.py:170: Failed __________________________ TestRaises.test_raise ___________________________ self = @@ -418,7 +418,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise ValueError("demo error") E ValueError: demo error - failure_demo.py:174: ValueError + failure_demo.py:173: ValueError ________________________ TestRaises.test_tupleerror ________________________ self = @@ -427,7 +427,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = [1] # NOQA E ValueError: not enough values to unpack (expected 2, got 1) - failure_demo.py:177: ValueError + failure_demo.py:176: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ self = @@ -438,7 +438,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items.pop() E TypeError: 'int' object is not iterable - failure_demo.py:182: TypeError + failure_demo.py:181: TypeError --------------------------- Captured stdout call --------------------------- items is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ @@ -449,7 +449,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > if namenotexi: # NOQA E NameError: name 'namenotexi' is not defined - failure_demo.py:185: NameError + failure_demo.py:184: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): @@ -464,14 +464,14 @@ Here is a nice run of several failures and how ``pytest`` presents things: sys.modules[name] = module > module.foo() - failure_demo.py:203: + failure_demo.py:202: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def foo(): > assert 1 == 0 E AssertionError - <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:200>:2: AssertionError + <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:199>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = @@ -485,9 +485,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: > somefunc(f(), g()) - failure_demo.py:214: + failure_demo.py:213: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - failure_demo.py:12: in somefunc + failure_demo.py:11: in somefunc otherfunc(x, y) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @@ -497,7 +497,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 44 == 43 - failure_demo.py:8: AssertionError + failure_demo.py:7: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ self = @@ -507,7 +507,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E ValueError: not enough values to unpack (expected 2, got 0) - failure_demo.py:218: ValueError + failure_demo.py:217: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -517,7 +517,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E TypeError: 'int' object is not iterable - failure_demo.py:222: TypeError + failure_demo.py:221: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -530,7 +530,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = ('456') E + where = '123'.startswith - failure_demo.py:227: AssertionError + failure_demo.py:226: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -549,7 +549,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where '123' = .f at 0xdeadbeef>() E + and '456' = .g at 0xdeadbeef>() - failure_demo.py:236: AssertionError + failure_demo.py:235: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -560,7 +560,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = isinstance(43, float) E + where 43 = globf(42) - failure_demo.py:239: AssertionError + failure_demo.py:238: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = @@ -571,7 +571,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 42 != 42 E + where 42 = .x - failure_demo.py:243: AssertionError + failure_demo.py:242: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = @@ -581,7 +581,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 11 < 5 E + where 11 = globf(10) - failure_demo.py:246: AssertionError + failure_demo.py:245: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -592,13 +592,13 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert x == 0 E assert 1 == 0 - failure_demo.py:251: AssertionError + failure_demo.py:250: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = def test_single_line(self): - class A(object): + class A: a = 1 b = 2 @@ -607,13 +607,13 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:262: AssertionError + failure_demo.py:261: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = def test_multiline(self): - class A(object): + class A: a = 1 b = 2 @@ -626,13 +626,13 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:269: AssertionError + failure_demo.py:268: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = def test_custom_repr(self): - class JSON(object): + class JSON: a = 1 def __repr__(self): @@ -648,5 +648,11 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a - failure_demo.py:282: AssertionError - ======================== 44 failed in 0.12 seconds ========================= + failure_demo.py:281: AssertionError + ============================= warnings summary ============================= + failure_demo.py::test_dynamic_compile_shows_nicely + $REGENDOC_TMPDIR/assertion/failure_demo.py:193: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses + import imp + + -- Docs: https://docs.pytest.org/en/latest/warnings.html + ================== 44 failed, 1 warnings in 0.12 seconds =================== From 5e39eb91bb6d7bdc11364ab741f1057d36fc17d5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 10:54:16 -0700 Subject: [PATCH 105/109] Correct Zac-HD's name in changelogs --- doc/en/announce/release-3.8.2.rst | 2 +- doc/en/announce/release-4.3.1.rst | 1 - doc/en/announce/release-4.5.0.rst | 1 - doc/en/announce/release-5.0.0.rst | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/en/announce/release-3.8.2.rst b/doc/en/announce/release-3.8.2.rst index 124c33aa40e..ecc47fbb33b 100644 --- a/doc/en/announce/release-3.8.2.rst +++ b/doc/en/announce/release-3.8.2.rst @@ -20,7 +20,7 @@ Thanks to all who contributed to this release, among them: * Jeffrey Rackauckas * Jose Carlos Menezes * Ronny Pfannschmidt -* Zac-HD +* Zac Hatfield-Dodds * iwanb diff --git a/doc/en/announce/release-4.3.1.rst b/doc/en/announce/release-4.3.1.rst index 45d14fffed9..54cf8b3fcd8 100644 --- a/doc/en/announce/release-4.3.1.rst +++ b/doc/en/announce/release-4.3.1.rst @@ -21,7 +21,6 @@ Thanks to all who contributed to this release, among them: * Kyle Altendorf * Stephan Hoyer * Zac Hatfield-Dodds -* Zac-HD * songbowen diff --git a/doc/en/announce/release-4.5.0.rst b/doc/en/announce/release-4.5.0.rst index 084579ac419..37c16cd7224 100644 --- a/doc/en/announce/release-4.5.0.rst +++ b/doc/en/announce/release-4.5.0.rst @@ -28,7 +28,6 @@ Thanks to all who contributed to this release, among them: * Pulkit Goyal * Samuel Searles-Bryant * Zac Hatfield-Dodds -* Zac-HD Happy testing, diff --git a/doc/en/announce/release-5.0.0.rst b/doc/en/announce/release-5.0.0.rst index a198c6fa6dc..ca516060215 100644 --- a/doc/en/announce/release-5.0.0.rst +++ b/doc/en/announce/release-5.0.0.rst @@ -38,7 +38,6 @@ Thanks to all who contributed to this release, among them: * Victor Maryama * Yuri Apollov * Zac Hatfield-Dodds -* Zac-HD * curiousjazz77 * patriksevallius From 55d2fe076f6b5a4a3b90d2df829685580dc48937 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 10:59:58 -0700 Subject: [PATCH 106/109] Use importlib instead of imp in demo --- doc/en/example/assertion/failure_demo.py | 5 +-- doc/en/example/reportingdemo.rst | 41 +++++++++++------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index 3a307816f00..129362cd7ff 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -190,12 +190,13 @@ def func1(self): # thanks to Matthew Scott for this test def test_dynamic_compile_shows_nicely(): - import imp + import importlib.util import sys src = "def foo():\n assert 1 == 0\n" name = "abc-123" - module = imp.new_module(name) + spec = importlib.util.spec_from_loader(name, loader=None) + module = importlib.util.module_from_spec(spec) code = _pytest._code.compile(src, name, "exec") exec(code, module.__dict__) sys.modules[name] = module diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 617681c1980..8212c8e2432 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -453,25 +453,26 @@ Here is a nice run of several failures and how ``pytest`` presents things: ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): - import imp + import importlib.util import sys src = "def foo():\n assert 1 == 0\n" name = "abc-123" - module = imp.new_module(name) + spec = importlib.util.spec_from_loader(name, loader=None) + module = importlib.util.module_from_spec(spec) code = _pytest._code.compile(src, name, "exec") exec(code, module.__dict__) sys.modules[name] = module > module.foo() - failure_demo.py:202: + failure_demo.py:203: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def foo(): > assert 1 == 0 E AssertionError - <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:199>:2: AssertionError + <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:200>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = @@ -485,7 +486,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > somefunc(f(), g()) - failure_demo.py:213: + failure_demo.py:214: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ failure_demo.py:11: in somefunc otherfunc(x, y) @@ -507,7 +508,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E ValueError: not enough values to unpack (expected 2, got 0) - failure_demo.py:217: ValueError + failure_demo.py:218: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -517,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E TypeError: 'int' object is not iterable - failure_demo.py:221: TypeError + failure_demo.py:222: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -530,7 +531,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = ('456') E + where = '123'.startswith - failure_demo.py:226: AssertionError + failure_demo.py:227: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -549,7 +550,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where '123' = .f at 0xdeadbeef>() E + and '456' = .g at 0xdeadbeef>() - failure_demo.py:235: AssertionError + failure_demo.py:236: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -560,7 +561,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = isinstance(43, float) E + where 43 = globf(42) - failure_demo.py:238: AssertionError + failure_demo.py:239: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = @@ -571,7 +572,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 42 != 42 E + where 42 = .x - failure_demo.py:242: AssertionError + failure_demo.py:243: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = @@ -581,7 +582,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 11 < 5 E + where 11 = globf(10) - failure_demo.py:245: AssertionError + failure_demo.py:246: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -592,7 +593,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert x == 0 E assert 1 == 0 - failure_demo.py:250: AssertionError + failure_demo.py:251: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = @@ -607,7 +608,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:261: AssertionError + failure_demo.py:262: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = @@ -626,7 +627,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:268: AssertionError + failure_demo.py:269: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = @@ -648,11 +649,5 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a - failure_demo.py:281: AssertionError - ============================= warnings summary ============================= - failure_demo.py::test_dynamic_compile_shows_nicely - $REGENDOC_TMPDIR/assertion/failure_demo.py:193: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses - import imp - - -- Docs: https://docs.pytest.org/en/latest/warnings.html - ================== 44 failed, 1 warnings in 0.12 seconds =================== + failure_demo.py:282: AssertionError + ======================== 44 failed in 0.12 seconds ========================= From 97f0a20ca9fe4e1acac7d3510441214a223f36ac Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 11:05:07 -0700 Subject: [PATCH 107/109] Add notice about py35+ and move ExitCode changelog entry --- CHANGELOG.rst | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 27d15a8754a..1ad0a155506 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,13 @@ with advance notice in the **Deprecations** section of releases. pytest 5.0.0 (2019-06-28) ========================= +Important +--------- + +This release is a Python3.5+ only release. + +For more details, see our `Python 2.7 and 3.4 support plan `__. + Removals -------- @@ -33,13 +40,6 @@ Removals `#4009 `__. -- `#5125 `_: ``Session.exitcode`` values are now coded in ``pytest.ExitCode``, an ``IntEnum``. This makes the exit code available for consumer code and are more explicit other than just documentation. User defined exit codes are still valid, but should be used with caution. - - The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios. - - **pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change. - - - `#5402 `_: **PytestDeprecationWarning are now errors by default.** Following our plan to remove deprecated features with as little disruption as @@ -104,6 +104,13 @@ Features - `#5452 `_: When warnings are configured as errors, pytest warnings now appear as originating from ``pytest.`` instead of the internal ``_pytest.warning_types.`` module. +- `#5125 `_: ``Session.exitcode`` values are now coded in ``pytest.ExitCode``, an ``IntEnum``. This makes the exit code available for consumer code and are more explicit other than just documentation. User defined exit codes are still valid, but should be used with caution. + + The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios. + + **pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change. + + Bug Fixes --------- From 58bfc7736fc4f88eca669157822e00715c67a9bf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 11:08:31 -0700 Subject: [PATCH 108/109] Use shutil.which to avoid distutils+imp warning --- doc/en/example/multipython.py | 4 ++-- doc/en/example/parametrize.rst | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index c06e452df26..3dc1d9b29d1 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -2,7 +2,7 @@ module containing a parametrized tests testing cross-python serialization via the pickle module. """ -import distutils.spawn +import shutil import subprocess import textwrap @@ -24,7 +24,7 @@ def python2(request, python1): class Python: def __init__(self, version, picklefile): - self.pythonpath = distutils.spawn.find_executable(version) + self.pythonpath = shutil.which(version) if not self.pythonpath: pytest.skip("{!r} not found".format(version)) self.picklefile = picklefile diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 8c0e4754f6d..a9f006f2425 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -435,15 +435,9 @@ Running it results in some skips if we don't have all the python interpreters in . $ pytest -rs -q multipython.py ssssssssssss......sss...... [100%] - ============================= warnings summary ============================= - $PYTHON_PREFIX/lib/python3.6/distutils/__init__.py:1 - $PYTHON_PREFIX/lib/python3.6/distutils/__init__.py:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses - import imp - - -- Docs: https://docs.pytest.org/en/latest/warnings.html ========================= short test summary info ========================== SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - 12 passed, 15 skipped, 1 warnings in 0.12 seconds + 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- From 844d660d5cab2c1c0ade5aba4b19ccc359154404 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 28 Jun 2019 19:09:10 -0700 Subject: [PATCH 109/109] Merge pull request #5520 from asottile/release-4.6.4 Preparing release version 4.6.4 --- CHANGELOG.rst | 20 ++++++++++++++++++++ doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.4.rst | 22 ++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 doc/en/announce/release-4.6.4.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1ad0a155506..6e5e8985af4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -203,6 +203,26 @@ Improved Documentation - `#5416 `_: Fix PytestUnknownMarkWarning in run/skip example. +pytest 4.6.4 (2019-06-28) +========================= + +Bug Fixes +--------- + +- `#5404 `_: Emit a warning when attempting to unwrap a broken object raises an exception, + for easier debugging (`#5080 `__). + + +- `#5444 `_: Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect. + + +- `#5482 `_: Fix bug introduced in 4.6.0 causing collection errors when passing + more than 2 positional arguments to ``pytest.mark.parametrize``. + + +- `#5505 `_: Fix crash when discovery fails while using ``-p no:terminal``. + + pytest 4.6.3 (2019-06-11) ========================= diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index c8247ceb37b..700e5361cf9 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -7,6 +7,7 @@ Release announcements release-5.0.0 + release-4.6.4 release-4.6.3 release-4.6.2 release-4.6.1 diff --git a/doc/en/announce/release-4.6.4.rst b/doc/en/announce/release-4.6.4.rst new file mode 100644 index 00000000000..7b35ed4f0d4 --- /dev/null +++ b/doc/en/announce/release-4.6.4.rst @@ -0,0 +1,22 @@ +pytest-4.6.4 +======================================= + +pytest 4.6.4 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Thomas Grainger +* Zac Hatfield-Dodds + + +Happy testing, +The pytest Development Team