Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a safeguard module to import stdlib objects from #3290

Closed
nicoddemus opened this issue Mar 7, 2018 · 19 comments
Closed

Create a safeguard module to import stdlib objects from #3290

nicoddemus opened this issue Mar 7, 2018 · 19 comments
Assignees
Labels
good first issue easy issue that is friendly to new contributor plugin: monkeypatch related to the monkeypatch builtin plugin status: help wanted developers would like help from experts on this topic type: bug problem that needs to be addressed

Comments

@nicoddemus
Copy link
Member

nicoddemus commented Mar 7, 2018

The rationale here being that users sometimes monkeypatch functions and objects from the standard library which can break pytest itself.

@RonnyPfannschmidt's idea from #3288 (comment).

Refs: #3288, #2180, #3352

@nicoddemus nicoddemus added type: bug problem that needs to be addressed status: help wanted developers would like help from experts on this topic type: task good first issue easy issue that is friendly to new contributor plugin: monkeypatch related to the monkeypatch builtin plugin labels Mar 7, 2018
@nicoddemus
Copy link
Member Author

To elaborate, the idea is that in normal pytest modules we never import from stdlib or external modules directly, but from a safeimp.py instead which imports all required symbols when pytest starts.

The issue is that sometimes people will monkey patch some stdlib module or builtin function, and this breaks pytest internals.

For example some pytest module (say python.py) might have this import:

import os

    # down the module...
    os.environ['PYTEST_CURRENT_TEST'] = node.id

Now if a user during a test does something like this:

import os
def test(monkeypatch):
    monkeypatch.setattr(os, 'environ', 1)

It will break pytest internally because os.environ will be 1 instead of a dict, which will break the code in python.py.

That is the problem, the solution @RonnyPfannschmidt suggests is to import all symbols we require early in a safeimp.py module:

# safeimp.py
from os import environ

So the code in python.py is changed to:

from .safeimp import environ

    # down the module...
    environ['PYTEST_CURRENT_TEST'] = node.id

So we get the original os.environ even if the user messes with it during the tests.

You might start with looking at all imports from the stdlib (like os.environ above) and move them to this new safeimp.py file. And don't worry if we miss some, it is then a matter of updating it with new symbols in the future, the important thing is to have this mechanism in-place.

@brianmaissy
Copy link
Contributor

Wouldn't this damage the readability of the code, because we would always have to refer to stdlib objects without their module?

@nicoddemus
Copy link
Member Author

@brianmaissy Definitely it would, and it would be a hassle to enforce that without a linter. 😬

@brianmaissy
Copy link
Contributor

Then I wonder if it's worth the bother. How common is the use case of someone monkeypatching the standard library so badly that it doesn't behave like the standard library anymore? I sort feel like you deserve what you get if you make os.environ something that doesn't behave reasonably like a mapping.

@feuillemorte feuillemorte self-assigned this Apr 4, 2018
@feuillemorte
Copy link
Contributor

feuillemorte commented Apr 4, 2018

@nicoddemus

Seems, it doesn't help or I doing smth wrong :(

test_1.py:

import functools

from unittest.mock import Mock


def test_partial(monkeypatch):
    monkeypatch.setattr(functools, "partial", Mock())
    assert functools.partial.call_count == 1

safeguarded_function.py:

import functools

compat.py:

from _pytest import safeguarded_function as sf
......
if isinstance(obj, sf.functools.partial):
....

output:

platform linux -- Python 3.6.4, pytest-3.5.1.dev26+gad0b4330.d20180404, py-1.5.3, pluggy-0.6.0
rootdir: /home/feuillemorte/github/pytest-dev/pytest/pytest_3290, inifile: tox.ini
collected 1 item                                                                                                                                                                       

tests/test_1.py 
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/main.py", line 107, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/main.py", line 145, in _main
INTERNALERROR>     config.hook.pytest_runtestloop(session=session)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 617, in __call__
INTERNALERROR>     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 222, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 216, in <lambda>
INTERNALERROR>     firstresult=hook.spec_opts.get('firstresult'),
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 201, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 76, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 180, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/main.py", line 168, in pytest_runtestloop
INTERNALERROR>     item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 617, in __call__
INTERNALERROR>     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 222, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 216, in <lambda>
INTERNALERROR>     firstresult=hook.spec_opts.get('firstresult'),
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 201, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 76, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 180, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/runner.py", line 62, in pytest_runtest_protocol
INTERNALERROR>     runtestprotocol(item, nextitem=nextitem)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/runner.py", line 79, in runtestprotocol
INTERNALERROR>     reports.append(call_and_report(item, "call", log))
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/runner.py", line 160, in call_and_report
INTERNALERROR>     report = hook.pytest_runtest_makereport(item=item, call=call)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 617, in __call__
INTERNALERROR>     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 222, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/__init__.py", line 216, in <lambda>
INTERNALERROR>     firstresult=hook.spec_opts.get('firstresult'),
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 196, in _multicall
INTERNALERROR>     gen.send(outcome)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/skipping.py", line 117, in pytest_runtest_makereport
INTERNALERROR>     rep = outcome.get_result()
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 76, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pluggy-0.6.0-py3.6.egg/pluggy/callers.py", line 180, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/runner.py", line 312, in pytest_runtest_makereport
INTERNALERROR>     longrepr = item.repr_failure(excinfo)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/python.py", line 598, in repr_failure
INTERNALERROR>     return self._repr_failure_py(excinfo, style=style)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/python.py", line 591, in _repr_failure_py
INTERNALERROR>     style=style)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/nodes.py", line 229, in _repr_failure_py
INTERNALERROR>     self._prunetraceback(excinfo)
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/python.py", line 567, in _prunetraceback
INTERNALERROR>     code = _pytest._code.Code(get_real_func(self.obj))
INTERNALERROR>   File "/home/feuillemorte/github/pytest-dev/pytest/pytest_3290/env/lib/python3.6/site-packages/pytest-3.5.1.dev26+gad0b4330.d20180404-py3.6.egg/_pytest/compat.py", line 214, in get_real_func
INTERNALERROR>     if isinstance(obj, sf.functools.partial):
INTERNALERROR> TypeError: isinstance() arg 2 must be a type or tuple of types

@brianmaissy
Copy link
Contributor

@feuillemorte You need to import partial itself in safeguarded_function, not justfunctools. You are monkeypatching the same module which you have already imported.

@feuillemorte
Copy link
Contributor

@brianmaissy I see, thank you!

@twmr
Copy link
Contributor

twmr commented Apr 4, 2018

Note that this safeimpl approach fails when tests mock methods/attributes of classes from the stdlibrary!

@nicoddemus
Copy link
Member Author

@Thisch that's true, thanks for pointing it out. If anybody has any other suggestions they are welcome though, the outline above is what I got from @RonnyPfannschmidt's suggestion.

@RonnyPfannschmidt
Copy link
Member

we can definitively pick a service level for patching the stdlib tho

  • replace module level objects and you will be fine, go for the internals aand you are on your own

@nicoddemus
Copy link
Member Author

@brianmaissy

Then I wonder if it's worth the bother. How common is the use case of someone monkeypatching the standard library so badly that it doesn't behave like the standard library anymore? I sort feel like you deserve what you get if you make os.environ something that doesn't behave reasonably like a mapping.

You got a point and I kinda agree with you. The problem is that people sometimes do it (for legitimate testing reasons), and they waste a lot of time trying to figure out what happened. If we can save those users time by implementing some safe-guard, I think it is worth doing it. My 2 cents.

@feuillemorte
Copy link
Contributor

feuillemorte commented Apr 6, 2018

What if I import it like

from functools import partial as functools_partial
from inspect import isclass as inspect_isclass, CO_VARARGS as inspect_CO_VARARGS, \
    CO_VARKEYWORDS as inspect_CO_VARKEYWORDS
from sys import exc_info as sys_exc_info

then this will easier to understand what is it:

from _pytest.safeguarded_function import functools_partial
.....
    if isinstance(obj, functools_partial):
        obj = obj.func
    return obj

Because it's really hard to understand what I import:

from _pytest.safeguarded_function import isclass, CO_VARARGS, CO_VARKEYWORDS, exc_info, ref, ib, Factory, s

Also, file safeguarded_function.py maybe huge:

from functools import \
    partial as functools_partial
from inspect import \
    isclass as inspect_isclass,\
    CO_VARARGS as inspect_CO_VARARGS,\
    CO_VARKEYWORDS as inspect_CO_VARKEYWORDS
from sys import \
    exc_info as sys_exc_info
from attr import \
    ib as attr_ib, \
    s as attr_s, \
    Factory as attr_Factory
from weakref import \
    ref as weakref_ref
from re import \
    search as re_search

@nicoddemus
Copy link
Member Author

Looking at the implementation so far, I'm starting to have second thoughts about this myself because it seems we will be adding a lot of overhead to our development.

Let's backtrack a little, to recap:

#3288: monkeypatch.setattr(functools, "partial", Mock())
#2180: with patch('builtins.open', mock_open(read_data='')):
#3352: monkeypatch.delattr(os, 'environ')

Except for #2180, the others could be fixed by using with mock.patch instead, because the mock would be undone after the with block.

If the above is true, we could instead improve monkeypatch to support some form of with statement, which would be an alternative for #3288 and #3352. To handle #2180, we could just import open and other builtins explicitly at the module level of rewrite.py to shield us from users tampering with it.

Something like:

with monkeypatch.context() as m:
    m.setattr(functools, "partial", Mock())

(context is not a good name I think, just using it to illustrate the point).

@RonnyPfannschmidt
Copy link
Member

i agree - lets document that monkeypatching the stdlib without care can and will break pytest

@nicoddemus
Copy link
Member Author

Sounds good, and what do you think about the suggestions I made above?

@RonnyPfannschmidt
Copy link
Member

the "i agree" was targetted at those, its a good starting point and i believe with a few iterations we will arrive at something fabulous

@feuillemorte
Copy link
Contributor

class MonkeyPatch(object):
    def __enter__(self):
        print('HEY OPEN!')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('HEY CLOSE!')
def test_partial(monkeypatch):
    with monkeypatch as m:
        m.setattr(functools, "partial", Mock())
        assert functools.partial.call_count == 1

output:

--------------------------------------------------------------------------------- Captured stdout call ---------------------------------------------------------------------------------
HEY OPEN!
HEY CLOSE!

But how to create another namespace for with statement? I don't understand how i can save functools in this case

@RonnyPfannschmidt
Copy link
Member

the new namespace is the return value - the monkeypatcher nesting should be done as a separate feature tool and should be used for the capture plugin as well

@feuillemorte
Copy link
Contributor

Merged. So, I'm closing the task

guykisel pushed a commit to guykisel/inline-plz that referenced this issue Jul 6, 2018
This PR updates [pytest](https://pypi.org/project/pytest) from **3.5.1** to **3.6.3**.



<details>
  <summary>Changelog</summary>
  
  
   ### 3.6.2
   ```
   =========================

Bug Fixes
---------

- Fix regression in ``Node.add_marker`` by extracting the mark object of a
  ``MarkDecorator``. (`3555
  &lt;https://github.com/pytest-dev/pytest/issues/3555&gt;`_)

- Warnings without ``location`` were reported as ``None``. This is corrected to
  now report ``&lt;undetermined location&gt;``. (`3563
  &lt;https://github.com/pytest-dev/pytest/issues/3563&gt;`_)

- Continue to call finalizers in the stack when a finalizer in a former scope
  raises an exception. (`3569
  &lt;https://github.com/pytest-dev/pytest/issues/3569&gt;`_)

- Fix encoding error with `print` statements in doctests (`3583
  &lt;https://github.com/pytest-dev/pytest/issues/3583&gt;`_)


Improved Documentation
----------------------

- Add documentation for the ``--strict`` flag. (`3549
  &lt;https://github.com/pytest-dev/pytest/issues/3549&gt;`_)


Trivial/Internal Changes
------------------------

- Update old quotation style to parens in fixture.rst documentation. (`3525
  &lt;https://github.com/pytest-dev/pytest/issues/3525&gt;`_)

- Improve display of hint about ``--fulltrace`` with ``KeyboardInterrupt``.
  (`3545 &lt;https://github.com/pytest-dev/pytest/issues/3545&gt;`_)

- pytest&#39;s testsuite is no longer runnable through ``python setup.py test`` --
  instead invoke ``pytest`` or ``tox`` directly. (`3552
  &lt;https://github.com/pytest-dev/pytest/issues/3552&gt;`_)

- Fix typo in documentation (`3567
  &lt;https://github.com/pytest-dev/pytest/issues/3567&gt;`_)
   ```
   
  
  
   ### 3.6.1
   ```
   =========================

Bug Fixes
---------

- Fixed a bug where stdout and stderr were logged twice by junitxml when a test
  was marked xfail. (`3491
  &lt;https://github.com/pytest-dev/pytest/issues/3491&gt;`_)

- Fix ``usefixtures`` mark applyed to unittest tests by correctly instantiating
  ``FixtureInfo``. (`3498
  &lt;https://github.com/pytest-dev/pytest/issues/3498&gt;`_)

- Fix assertion rewriter compatibility with libraries that monkey patch
  ``file`` objects. (`3503
  &lt;https://github.com/pytest-dev/pytest/issues/3503&gt;`_)


Improved Documentation
----------------------

- Added a section on how to use fixtures as factories to the fixture
  documentation. (`3461 &lt;https://github.com/pytest-dev/pytest/issues/3461&gt;`_)


Trivial/Internal Changes
------------------------

- Enable caching for pip/pre-commit in order to reduce build time on
  travis/appveyor. (`3502
  &lt;https://github.com/pytest-dev/pytest/issues/3502&gt;`_)

- Switch pytest to the src/ layout as we already suggested it for good practice
  - now we implement it as well. (`3513
  &lt;https://github.com/pytest-dev/pytest/issues/3513&gt;`_)

- Fix if in tests to support 3.7.0b5, where a docstring handling in AST got
  reverted. (`3530 &lt;https://github.com/pytest-dev/pytest/issues/3530&gt;`_)

- Remove some python2.5 compatibility code. (`3529
  &lt;https://github.com/pytest-dev/pytest/issues/3529&gt;`_)
   ```
   
  
  
   ### 3.6.0
   ```
   =========================

Features
--------

- Revamp the internals of the ``pytest.mark`` implementation with correct per
  node handling which fixes a number of long standing bugs caused by the old
  design. This introduces new ``Node.iter_markers(name)`` and
  ``Node.get_closest_mark(name)`` APIs. Users are **strongly encouraged** to
  read the `reasons for the revamp in the docs
  &lt;https://docs.pytest.org/en/latest/mark.htmlmarker-revamp-and-iteration&gt;`_,
  or jump over to details about `updating existing code to use the new APIs
  &lt;https://docs.pytest.org/en/latest/mark.htmlupdating-code&gt;`_. (`3317
  &lt;https://github.com/pytest-dev/pytest/issues/3317&gt;`_)

- Now when ``pytest.fixture`` is applied more than once to the same function a
  ``ValueError`` is raised. This buggy behavior would cause surprising problems
  and if was working for a test suite it was mostly by accident. (`2334
  &lt;https://github.com/pytest-dev/pytest/issues/2334&gt;`_)

- Support for Python 3.7&#39;s builtin ``breakpoint()`` method, see `Using the
  builtin breakpoint function
  &lt;https://docs.pytest.org/en/latest/usage.htmlbreakpoint-builtin&gt;`_ for
  details. (`3180 &lt;https://github.com/pytest-dev/pytest/issues/3180&gt;`_)

- ``monkeypatch`` now supports a ``context()`` function which acts as a context
  manager which undoes all patching done within the ``with`` block. (`3290
  &lt;https://github.com/pytest-dev/pytest/issues/3290&gt;`_)

- The ``--pdb`` option now causes KeyboardInterrupt to enter the debugger,
  instead of stopping the test session. On python 2.7, hitting CTRL+C again
  exits the debugger. On python 3.2 and higher, use CTRL+D. (`3299
  &lt;https://github.com/pytest-dev/pytest/issues/3299&gt;`_)

- pytest not longer changes the log level of the root logger when the
  ``log-level`` parameter has greater numeric value than that of the level of
  the root logger, which makes it play better with custom logging configuration
  in user code. (`3307 &lt;https://github.com/pytest-dev/pytest/issues/3307&gt;`_)


Bug Fixes
---------

- A rare race-condition which might result in corrupted ``.pyc`` files on
  Windows has been hopefully solved. (`3008
  &lt;https://github.com/pytest-dev/pytest/issues/3008&gt;`_)

- Also use iter_marker for discovering the marks applying for marker
  expressions from the cli to avoid the bad data from the legacy mark storage.
  (`3441 &lt;https://github.com/pytest-dev/pytest/issues/3441&gt;`_)

- When showing diffs of failed assertions where the contents contain only
  whitespace, escape them using ``repr()`` first to make it easy to spot the
  differences. (`3443 &lt;https://github.com/pytest-dev/pytest/issues/3443&gt;`_)


Improved Documentation
----------------------

- Change documentation copyright year to a range which auto-updates itself each
  time it is published. (`3303
  &lt;https://github.com/pytest-dev/pytest/issues/3303&gt;`_)


Trivial/Internal Changes
------------------------

- ``pytest`` now depends on the `python-atomicwrites
  &lt;https://github.com/untitaker/python-atomicwrites&gt;`_ library. (`3008
  &lt;https://github.com/pytest-dev/pytest/issues/3008&gt;`_)

- Update all pypi.python.org URLs to pypi.org. (`3431
  &lt;https://github.com/pytest-dev/pytest/issues/3431&gt;`_)

- Detect `pytest_` prefixed hooks using the internal plugin manager since
  ``pluggy`` is deprecating the ``implprefix`` argument to ``PluginManager``.
  (`3487 &lt;https://github.com/pytest-dev/pytest/issues/3487&gt;`_)

- Import ``Mapping`` and ``Sequence`` from ``_pytest.compat`` instead of
  directly from ``collections`` in ``python_api.py::approx``. Add ``Mapping``
  to ``_pytest.compat``, import it from ``collections`` on python 2, but from
  ``collections.abc`` on Python 3 to avoid a ``DeprecationWarning`` on Python
  3.7 or newer. (`3497 &lt;https://github.com/pytest-dev/pytest/issues/3497&gt;`_)
   ```
   
  
</details>


 

<details>
  <summary>Links</summary>
  
  - PyPI: https://pypi.org/project/pytest
  - Changelog: https://pyup.io/changelogs/pytest/
  - Repo: https://github.com/pytest-dev/pytest/issues
  - Homepage: http://pytest.org
</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue easy issue that is friendly to new contributor plugin: monkeypatch related to the monkeypatch builtin plugin status: help wanted developers would like help from experts on this topic type: bug problem that needs to be addressed
Projects
None yet
Development

No branches or pull requests

5 participants