Merge pull request #3931 from nicoddemus/internal-warnings

Use standard warnings for internal pytest warnings
This commit is contained in:
Bruno Oliveira 2018-09-05 14:05:52 -03:00 committed by GitHub
commit 531b76a513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1041 additions and 377 deletions

View File

@ -0,0 +1,5 @@
Internal pytest warnings are now issued using the standard ``warnings`` module, making it possible to use
the standard warnings filters to manage those warnings. This introduces ``PytestWarning``,
``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API.
Consult `the documentation <https://docs.pytest.org/en/latest/warnings.html#internal-pytest-warnings>`_ for more info.

View File

@ -0,0 +1,12 @@
``Config.warn`` has been deprecated, it should be replaced by calls to the standard ``warnings.warn``.
``Node.warn`` now supports two signatures:
* ``node.warn(PytestWarning("some message"))``: is now the recommended way to call this function. The warning
instance must be a ``PytestWarning`` or subclass instance.
* ``node.warn("CI", "some message")``: this code/message form is now deprecated and should be converted to
the warning instance form above.
``RemovedInPytest4Warning`` and ``PytestExperimentalApiWarning`` are now part of the public API and should be accessed
using ``pytest.RemovedInPytest4Warning`` and ``pytest.PytestExperimentalApiWarning``.

View File

@ -0,0 +1,5 @@
``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is
configured. This makes pytest more compliant with
`PEP-0506 <https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners>`_. See
`the docs <https://docs.pytest.org/en/latest/warnings.html#deprecationwarning-and-pendingdeprecationwarning>`_ for
more info.

View File

@ -0,0 +1 @@
Warnings are now captured and displayed during test collection.

View File

@ -0,0 +1,5 @@
``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped,
making it possible to actually use regular expressions to check the warning message.
**Note**: regex-escaping the match string was an implementation oversight that might break test suites which depend
on the old behavior.

View File

@ -611,6 +611,8 @@ Session related reporting hooks:
.. autofunction:: pytest_terminal_summary .. autofunction:: pytest_terminal_summary
.. autofunction:: pytest_fixture_setup .. autofunction:: pytest_fixture_setup
.. autofunction:: pytest_fixture_post_finalizer .. autofunction:: pytest_fixture_post_finalizer
.. autofunction:: pytest_logwarning
.. autofunction:: pytest_warning_captured
And here is the central hook for reporting about And here is the central hook for reporting about
test execution: test execution:

View File

@ -36,8 +36,6 @@ Running pytest now produces this output::
-- Docs: https://docs.pytest.org/en/latest/warnings.html -- Docs: https://docs.pytest.org/en/latest/warnings.html
=================== 1 passed, 1 warnings in 0.12 seconds =================== =================== 1 passed, 1 warnings in 0.12 seconds ===================
Pytest by default catches all warnings except for ``DeprecationWarning`` and ``PendingDeprecationWarning``.
The ``-W`` flag can be passed to control which warnings will be displayed or even turn The ``-W`` flag can be passed to control which warnings will be displayed or even turn
them into errors:: them into errors::
@ -78,6 +76,53 @@ Both ``-W`` command-line option and ``filterwarnings`` ini option are based on P
`-W option`_ and `warnings.simplefilter`_, so please refer to those sections in the Python `-W option`_ and `warnings.simplefilter`_, so please refer to those sections in the Python
documentation for other examples and advanced usage. documentation for other examples and advanced usage.
Disabling warning summary
-------------------------
Although not recommended, you can use the ``--disable-warnings`` command-line option to suppress the
warning summary entirely from the test run output.
Disabling warning capture entirely
----------------------------------
This plugin is enabled by default but can be disabled entirely in your ``pytest.ini`` file with:
.. code-block:: ini
[pytest]
addopts = -p no:warnings
Or passing ``-p no:warnings`` in the command-line. This might be useful if your test suites handles warnings
using an external system.
.. _`deprecation-warnings`:
DeprecationWarning and PendingDeprecationWarning
------------------------------------------------
.. versionadded:: 3.8
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters
are configured.
To disable showing ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings, you might define any warnings
filter either in the command-line or in the ini file, or you can use:
.. code-block:: ini
[pytest]
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
.. note::
This makes pytest more compliant with `PEP-0506 <https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners>`_ which suggests that those warnings should
be shown by default by test runners, but pytest doesn't follow ``PEP-0506`` completely because resetting all
warning filters like suggested in the PEP will break existing test suites that configure warning filters themselves
by calling ``warnings.simplefilter`` (see issue `#2430 <https://github.com/pytest-dev/pytest/issues/2430>`_
for an example of that).
.. _`filterwarnings`: .. _`filterwarnings`:
@ -144,18 +189,6 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
.. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings .. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings
Disabling warning capture
-------------------------
This feature is enabled by default but can be disabled entirely in your ``pytest.ini`` file with:
.. code-block:: ini
[pytest]
addopts = -p no:warnings
Or passing ``-p no:warnings`` in the command-line.
.. _`asserting warnings`: .. _`asserting warnings`:
.. _assertwarnings: .. _assertwarnings:
@ -296,3 +329,53 @@ You can also use it as a contextmanager::
def test_global(): def test_global():
with pytest.deprecated_call(): with pytest.deprecated_call():
myobject.deprecated_method() myobject.deprecated_method()
Internal pytest warnings
------------------------
.. versionadded:: 3.8
pytest may generate its own warnings in some situations, such as improper usage or deprecated features.
For example, pytest will emit a warning if it encounters a class that matches :confval:`python_classes` but also
defines an ``__init__`` constructor, as this prevents the class from being instantiated:
.. code-block:: python
# content of test_pytest_warnings.py
class Test:
def __init__(self):
pass
def test_foo(self):
assert 1 == 1
::
$ pytest test_pytest_warnings.py -q
======================================== warnings summary =========================================
test_pytest_warnings.py:1
$REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor
class Test:
-- Docs: http://doc.pytest.org/en/latest/warnings.html
1 warnings in 0.01 seconds
These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings.
Following our :ref:`backwards-compatibility`, deprecated features will be kept *at least* two minor releases. After that,
they will changed so they by default raise errors instead of just warnings, so users can adapt to it on their own time
if not having done so until now. In a later release the deprecated feature will be removed completely.
The following warning types ares used by pytest and are part of the public API:
.. autoclass:: pytest.PytestWarning
.. autoclass:: pytest.PytestDeprecationWarning
.. autoclass:: pytest.RemovedInPytest4Warning
.. autoclass:: pytest.PytestExperimentalApiWarning

View File

@ -419,7 +419,7 @@ additionally it is possible to copy examples for a example folder before running
============================= warnings summary ============================= ============================= warnings summary =============================
test_example.py::test_plugin test_example.py::test_plugin
$REGENDOC_TMPDIR/test_example.py:4: PytestExerimentalApiWarning: testdir.copy_example is an experimental api that may change over time $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
testdir.copy_example("test_example.py") testdir.copy_example("test_example.py")
-- Docs: https://docs.pytest.org/en/latest/warnings.html -- Docs: https://docs.pytest.org/en/latest/warnings.html

View File

@ -209,8 +209,12 @@ class AssertionRewritingHook(object):
self._must_rewrite.update(names) self._must_rewrite.update(names)
def _warn_already_imported(self, name): def _warn_already_imported(self, name):
self.config.warn( from _pytest.warning_types import PytestWarning
"P1", "Module already imported so cannot be rewritten: %s" % name from _pytest.warnings import _issue_config_warning
_issue_config_warning(
PytestWarning("Module already imported so cannot be rewritten: %s" % name),
self.config,
) )
def load_module(self, name): def load_module(self, name):
@ -746,13 +750,17 @@ class AssertionRewriter(ast.NodeVisitor):
the expression is false. the expression is false.
""" """
if isinstance(assert_.test, ast.Tuple) and self.config is not None: if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
fslocation = (self.module_path, assert_.lineno) from _pytest.warning_types import PytestWarning
self.config.warn( import warnings
"R1",
"assertion is always true, perhaps " "remove parentheses?", warnings.warn_explicit(
fslocation=fslocation, PytestWarning("assertion is always true, perhaps remove parentheses?"),
category=None,
filename=str(self.module_path),
lineno=assert_.lineno,
) )
self.statements = [] self.statements = []
self.variables = [] self.variables = []
self.variable_counter = itertools.count() self.variable_counter = itertools.count()

View File

@ -33,7 +33,7 @@ See [the docs](https://docs.pytest.org/en/latest/cache.html) for more informatio
@attr.s @attr.s
class Cache(object): class Cache(object):
_cachedir = attr.ib(repr=False) _cachedir = attr.ib(repr=False)
_warn = attr.ib(repr=False) _config = attr.ib(repr=False)
@classmethod @classmethod
def for_config(cls, config): def for_config(cls, config):
@ -41,14 +41,19 @@ class Cache(object):
if config.getoption("cacheclear") and cachedir.exists(): if config.getoption("cacheclear") and cachedir.exists():
shutil.rmtree(str(cachedir)) shutil.rmtree(str(cachedir))
cachedir.mkdir() cachedir.mkdir()
return cls(cachedir, config.warn) return cls(cachedir, config)
@staticmethod @staticmethod
def cache_dir_from_config(config): def cache_dir_from_config(config):
return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir) return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir)
def warn(self, fmt, **args): def warn(self, fmt, **args):
self._warn(code="I9", message=fmt.format(**args) if args else fmt) from _pytest.warnings import _issue_config_warning
from _pytest.warning_types import PytestWarning
_issue_config_warning(
PytestWarning(fmt.format(**args) if args else fmt), self._config
)
def makedir(self, name): def makedir(self, name):
""" return a directory path object with the given name. If the """ return a directory path object with the given name. If the

View File

@ -176,7 +176,9 @@ def _prepareconfig(args=None, plugins=None):
else: else:
pluginmanager.register(plugin) pluginmanager.register(plugin)
if warning: if warning:
config.warn("C1", warning) from _pytest.warnings import _issue_config_warning
_issue_config_warning(warning, config=config)
return pluginmanager.hook.pytest_cmdline_parse( return pluginmanager.hook.pytest_cmdline_parse(
pluginmanager=pluginmanager, args=args pluginmanager=pluginmanager, args=args
) )
@ -417,7 +419,12 @@ class PytestPluginManager(PluginManager):
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
) )
warnings.warn(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST) warnings.warn_explicit(
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST,
category=None,
filename=str(conftestpath),
lineno=0,
)
except Exception: except Exception:
raise ConftestImportFailure(conftestpath, sys.exc_info()) raise ConftestImportFailure(conftestpath, sys.exc_info())
@ -602,7 +609,29 @@ class Config(object):
fin() fin()
def warn(self, code, message, fslocation=None, nodeid=None): def warn(self, code, message, fslocation=None, nodeid=None):
""" generate a warning for this test session. """ """
.. deprecated:: 3.8
Use :py:func:`warnings.warn` or :py:func:`warnings.warn_explicit` directly instead.
Generate a warning for this test session.
"""
from _pytest.warning_types import RemovedInPytest4Warning
if isinstance(fslocation, (tuple, list)) and len(fslocation) > 2:
filename, lineno = fslocation[:2]
else:
filename = "unknown file"
lineno = 0
msg = "config.warn has been deprecated, use warnings.warn instead"
if nodeid:
msg = "{}: {}".format(nodeid, msg)
warnings.warn_explicit(
RemovedInPytest4Warning(msg),
category=None,
filename=filename,
lineno=lineno,
)
self.hook.pytest_logwarning.call_historic( self.hook.pytest_logwarning.call_historic(
kwargs=dict( kwargs=dict(
code=code, message=message, fslocation=fslocation, nodeid=nodeid code=code, message=message, fslocation=fslocation, nodeid=nodeid
@ -667,8 +696,8 @@ class Config(object):
r = determine_setup( r = determine_setup(
ns.inifilename, ns.inifilename,
ns.file_or_dir + unknown_args, ns.file_or_dir + unknown_args,
warnfunc=self.warn,
rootdir_cmd_arg=ns.rootdir or None, rootdir_cmd_arg=ns.rootdir or None,
config=self,
) )
self.rootdir, self.inifile, self.inicfg = r self.rootdir, self.inifile, self.inicfg = r
self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["rootdir"] = self.rootdir

View File

@ -10,15 +10,12 @@ def exists(path, ignore=EnvironmentError):
return False return False
def getcfg(args, warnfunc=None): def getcfg(args, config=None):
""" """
Search the list of arguments for a valid ini-file for pytest, Search the list of arguments for a valid ini-file for pytest,
and return a tuple of (rootdir, inifile, cfg-dict). and return a tuple of (rootdir, inifile, cfg-dict).
note: warnfunc is an optional function used to warn note: config is optional and used only to issue warnings explicitly (#2891).
about ini-files that use deprecated features.
This parameter should be removed when pytest
adopts standard deprecation warnings (#1804).
""" """
from _pytest.deprecated import CFG_PYTEST_SECTION from _pytest.deprecated import CFG_PYTEST_SECTION
@ -34,9 +31,15 @@ def getcfg(args, warnfunc=None):
if exists(p): if exists(p):
iniconfig = py.iniconfig.IniConfig(p) iniconfig = py.iniconfig.IniConfig(p)
if "pytest" in iniconfig.sections: if "pytest" in iniconfig.sections:
if inibasename == "setup.cfg" and warnfunc: if inibasename == "setup.cfg" and config is not None:
warnfunc( from _pytest.warnings import _issue_config_warning
"C1", CFG_PYTEST_SECTION.format(filename=inibasename) from _pytest.warning_types import RemovedInPytest4Warning
_issue_config_warning(
RemovedInPytest4Warning(
CFG_PYTEST_SECTION.format(filename=inibasename)
),
config=config,
) )
return base, p, iniconfig["pytest"] return base, p, iniconfig["pytest"]
if ( if (
@ -95,7 +98,7 @@ def get_dirs_from_args(args):
return [get_dir_from_path(path) for path in possible_paths if path.exists()] return [get_dir_from_path(path) for path in possible_paths if path.exists()]
def determine_setup(inifile, args, warnfunc=None, rootdir_cmd_arg=None): def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None):
dirs = get_dirs_from_args(args) dirs = get_dirs_from_args(args)
if inifile: if inifile:
iniconfig = py.iniconfig.IniConfig(inifile) iniconfig = py.iniconfig.IniConfig(inifile)
@ -105,23 +108,30 @@ def determine_setup(inifile, args, warnfunc=None, rootdir_cmd_arg=None):
for section in sections: for section in sections:
try: try:
inicfg = iniconfig[section] inicfg = iniconfig[section]
if is_cfg_file and section == "pytest" and warnfunc: if is_cfg_file and section == "pytest" and config is not None:
from _pytest.deprecated import CFG_PYTEST_SECTION from _pytest.deprecated import CFG_PYTEST_SECTION
from _pytest.warning_types import RemovedInPytest4Warning
from _pytest.warnings import _issue_config_warning
warnfunc("C1", CFG_PYTEST_SECTION.format(filename=str(inifile))) _issue_config_warning(
RemovedInPytest4Warning(
CFG_PYTEST_SECTION.format(filename=str(inifile))
),
config,
)
break break
except KeyError: except KeyError:
inicfg = None inicfg = None
rootdir = get_common_ancestor(dirs) rootdir = get_common_ancestor(dirs)
else: else:
ancestor = get_common_ancestor(dirs) ancestor = get_common_ancestor(dirs)
rootdir, inifile, inicfg = getcfg([ancestor], warnfunc=warnfunc) rootdir, inifile, inicfg = getcfg([ancestor], config=config)
if rootdir is None: if rootdir is None:
for rootdir in ancestor.parts(reverse=True): for rootdir in ancestor.parts(reverse=True):
if rootdir.join("setup.py").exists(): if rootdir.join("setup.py").exists():
break break
else: else:
rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc) rootdir, inifile, inicfg = getcfg(dirs, config=config)
if rootdir is None: if rootdir is None:
rootdir = get_common_ancestor([py.path.local(), ancestor]) rootdir = get_common_ancestor([py.path.local(), ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"

View File

@ -7,14 +7,16 @@ be removed when the time comes.
""" """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
from _pytest.warning_types import RemovedInPytest4Warning
class RemovedInPytest4Warning(DeprecationWarning): MAIN_STR_ARGS = RemovedInPytest4Warning(
"""warning class for features removed in pytest 4.0""" "passing a string to pytest.main() is deprecated, "
"pass a list of arguments instead."
)
YIELD_TESTS = RemovedInPytest4Warning(
MAIN_STR_ARGS = "passing a string to pytest.main() is deprecated, " "pass a list of arguments instead." "yield tests are deprecated, and scheduled to be removed in pytest 4.0"
)
YIELD_TESTS = "yield tests are deprecated, and scheduled to be removed in pytest 4.0"
FUNCARG_PREFIX = ( FUNCARG_PREFIX = (
'{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated '
@ -23,7 +25,7 @@ FUNCARG_PREFIX = (
) )
FIXTURE_FUNCTION_CALL = ( FIXTURE_FUNCTION_CALL = (
"Fixture {name} called directly. Fixtures are not meant to be called directly, " 'Fixture "{name}" called directly. Fixtures are not meant to be called directly, '
"are created automatically when test functions request them as parameters. " "are created automatically when test functions request them as parameters. "
"See https://docs.pytest.org/en/latest/fixture.html for more information." "See https://docs.pytest.org/en/latest/fixture.html for more information."
) )
@ -32,7 +34,7 @@ CFG_PYTEST_SECTION = (
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead." "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
) )
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue" GETFUNCARGVALUE = "getfuncargvalue is deprecated, use getfixturevalue"
RESULT_LOG = ( RESULT_LOG = (
"--result-log is deprecated and scheduled for removal in pytest 4.0.\n" "--result-log is deprecated and scheduled for removal in pytest 4.0.\n"
@ -51,7 +53,11 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html" "For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
) )
RECORD_XML_PROPERTY = ( NODE_WARN = RemovedInPytest4Warning(
"Node.warn(code, message) form has been deprecated, use Node.warn(warning_instance) instead."
)
RECORD_XML_PROPERTY = RemovedInPytest4Warning(
'Fixture renamed from "record_xml_property" to "record_property" as user ' 'Fixture renamed from "record_xml_property" to "record_property" as user '
"properties are now available to all reporters.\n" "properties are now available to all reporters.\n"
'"record_xml_property" is now deprecated.' '"record_xml_property" is now deprecated.'
@ -61,7 +67,7 @@ COLLECTOR_MAKEITEM = RemovedInPytest4Warning(
"pycollector makeitem was removed " "as it is an accidentially leaked internal api" "pycollector makeitem was removed " "as it is an accidentially leaked internal api"
) )
METAFUNC_ADD_CALL = ( METAFUNC_ADD_CALL = RemovedInPytest4Warning(
"Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n" "Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n"
"Please use Metafunc.parametrize instead." "Please use Metafunc.parametrize instead."
) )

View File

@ -1,13 +0,0 @@
class PytestExerimentalApiWarning(FutureWarning):
"warning category used to denote experiments in pytest"
@classmethod
def simple(cls, apiname):
return cls(
"{apiname} is an experimental api that may change over time".format(
apiname=apiname
)
)
PYTESTER_COPY_EXAMPLE = PytestExerimentalApiWarning.simple("testdir.copy_example")

View File

@ -1257,6 +1257,8 @@ class FixtureManager(object):
items[:] = reorder_items(items) items[:] = reorder_items(items)
def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
from _pytest import deprecated
if nodeid is not NOTSET: if nodeid is not NOTSET:
holderobj = node_or_obj holderobj = node_or_obj
else: else:
@ -1279,10 +1281,15 @@ class FixtureManager(object):
if not callable(obj): if not callable(obj):
continue continue
marker = defaultfuncargprefixmarker marker = defaultfuncargprefixmarker
from _pytest import deprecated
self.config.warn( filename, lineno = getfslineno(obj)
"C1", deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid warnings.warn_explicit(
RemovedInPytest4Warning(
deprecated.FUNCARG_PREFIX.format(name=name)
),
category=None,
filename=str(filename),
lineno=lineno + 1,
) )
name = name[len(self._argprefix) :] name = name[len(self._argprefix) :]
elif not isinstance(marker, FixtureFunctionMarker): elif not isinstance(marker, FixtureFunctionMarker):

View File

@ -526,7 +526,17 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
@hookspec(historic=True) @hookspec(historic=True)
def pytest_logwarning(message, code, nodeid, fslocation): def pytest_logwarning(message, code, nodeid, fslocation):
""" process a warning specified by a message, a code string, """
.. deprecated:: 3.8
This hook is will stop working in a future release.
pytest no longer triggers this hook, but the
terminal writer still implements it to display warnings issued by
:meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be
an error in future releases.
process a warning specified by a message, a code string,
a nodeid and fslocation (both of which may be None a nodeid and fslocation (both of which may be None
if the warning is not tied to a particular node/location). if the warning is not tied to a particular node/location).
@ -535,6 +545,27 @@ def pytest_logwarning(message, code, nodeid, fslocation):
""" """
@hookspec(historic=True)
def pytest_warning_captured(warning_message, when, item):
"""
Process a warning captured by the internal pytest warnings plugin.
:param warnings.WarningMessage warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
the same attributes as the parameters of :py:func:`warnings.showwarning`.
:param str when:
Indicates when the warning was captured. Possible values:
* ``"config"``: during pytest configuration/initialization stage.
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
:param pytest.Item|None item:
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
"""
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# doctest hooks # doctest hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------

View File

@ -258,12 +258,11 @@ def record_property(request):
@pytest.fixture @pytest.fixture
def record_xml_property(record_property): def record_xml_property(record_property, request):
"""(Deprecated) use record_property.""" """(Deprecated) use record_property."""
import warnings
from _pytest import deprecated from _pytest import deprecated
warnings.warn(deprecated.RECORD_XML_PROPERTY, DeprecationWarning, stacklevel=2) request.node.warn(deprecated.RECORD_XML_PROPERTY)
return record_property return record_property
@ -274,9 +273,9 @@ def record_xml_attribute(request):
The fixture is callable with ``(name, value)``, with value being The fixture is callable with ``(name, value)``, with value being
automatically xml-encoded automatically xml-encoded
""" """
request.node.warn( from _pytest.warning_types import PytestWarning
code="C3", message="record_xml_attribute is an experimental feature"
) request.node.warn(PytestWarning("record_xml_attribute is an experimental feature"))
xml = getattr(request.config, "_xml", None) xml = getattr(request.config, "_xml", None)
if xml is not None: if xml is not None:
node_reporter = xml.node_reporter(request.node.nodeid) node_reporter = xml.node_reporter(request.node.nodeid)

View File

@ -65,7 +65,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
return cls(values, marks, id_) return cls(values, marks, id_)
@classmethod @classmethod
def extract_from(cls, parameterset, legacy_force_tuple=False): def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=False):
""" """
:param parameterset: :param parameterset:
a legacy style parameterset that may or may not be a tuple, a legacy style parameterset that may or may not be a tuple,
@ -75,6 +75,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
enforce tuple wrapping so single argument tuple values enforce tuple wrapping so single argument tuple values
don't get decomposed and break tests don't get decomposed and break tests
:param belonging_definition: the item that we will be extracting the parameters from.
""" """
if isinstance(parameterset, cls): if isinstance(parameterset, cls):
@ -93,20 +94,24 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
if legacy_force_tuple: if legacy_force_tuple:
argval = (argval,) argval = (argval,)
if newmarks: if newmarks and belonging_definition is not None:
warnings.warn(MARK_PARAMETERSET_UNPACKING) belonging_definition.warn(MARK_PARAMETERSET_UNPACKING)
return cls(argval, marks=newmarks, id=None) return cls(argval, marks=newmarks, id=None)
@classmethod @classmethod
def _for_parametrize(cls, argnames, argvalues, func, config): def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()] argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1 force_tuple = len(argnames) == 1
else: else:
force_tuple = False force_tuple = False
parameters = [ parameters = [
ParameterSet.extract_from(x, legacy_force_tuple=force_tuple) ParameterSet.extract_from(
x,
legacy_force_tuple=force_tuple,
belonging_definition=function_definition,
)
for x in argvalues for x in argvalues
] ]
del argvalues del argvalues

View File

@ -1,5 +1,6 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import os import os
import warnings
import six import six
import py import py
@ -7,6 +8,7 @@ import attr
import _pytest import _pytest
import _pytest._code import _pytest._code
from _pytest.compat import getfslineno
from _pytest.mark.structures import NodeKeywords, MarkInfo from _pytest.mark.structures import NodeKeywords, MarkInfo
@ -134,19 +136,98 @@ class Node(object):
def __repr__(self): def __repr__(self):
return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None)) return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None))
def warn(self, code, message): def warn(self, _code_or_warning=None, message=None, code=None):
""" generate a warning with the given code and message for this """Issue a warning for this item.
item. """
Warnings will be displayed after the test session, unless explicitly suppressed.
This can be called in two forms:
**Warning instance**
This was introduced in pytest 3.8 and uses the standard warning mechanism to issue warnings.
.. code-block:: python
node.warn(PytestWarning("some message"))
The warning instance must be a subclass of :class:`pytest.PytestWarning`.
**code/message (deprecated)**
This form was used in pytest prior to 3.8 and is considered deprecated. Using this form will emit another
warning about the deprecation:
.. code-block:: python
node.warn("CI", "some message")
:param Union[Warning,str] _code_or_warning:
warning instance or warning code (legacy). This parameter receives an underscore for backward
compatibility with the legacy code/message form, and will be replaced for something
more usual when the legacy form is removed.
:param Union[str,None] message: message to display when called in the legacy form.
:param str code: code for the warning, in legacy form when using keyword arguments.
:return:
"""
if message is None:
if _code_or_warning is None:
raise ValueError("code_or_warning must be given")
self._std_warn(_code_or_warning)
else:
if _code_or_warning and code:
raise ValueError(
"code_or_warning and code cannot both be passed to this function"
)
code = _code_or_warning or code
self._legacy_warn(code, message)
def _legacy_warn(self, code, message):
"""
.. deprecated:: 3.8
Use :meth:`Node.std_warn <_pytest.nodes.Node.std_warn>` instead.
Generate a warning with the given code and message for this item.
"""
from _pytest.deprecated import NODE_WARN
self._std_warn(NODE_WARN)
assert isinstance(code, str) assert isinstance(code, str)
fslocation = getattr(self, "location", None) fslocation = get_fslocation_from_item(self)
if fslocation is None:
fslocation = getattr(self, "fspath", None)
self.ihook.pytest_logwarning.call_historic( self.ihook.pytest_logwarning.call_historic(
kwargs=dict( kwargs=dict(
code=code, message=message, nodeid=self.nodeid, fslocation=fslocation code=code, message=message, nodeid=self.nodeid, fslocation=fslocation
) )
) )
def _std_warn(self, warning):
"""Issue a warning for this item.
Warnings will be displayed after the test session, unless explicitly suppressed
:param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
:raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
"""
from _pytest.warning_types import PytestWarning
if not isinstance(warning, PytestWarning):
raise ValueError(
"warning must be an instance of PytestWarning or subclass, got {!r}".format(
warning
)
)
path, lineno = get_fslocation_from_item(self)
warnings.warn_explicit(
warning,
category=None,
filename=str(path),
lineno=lineno + 1 if lineno is not None else None,
)
# methods for ordering nodes # methods for ordering nodes
@property @property
def nodeid(self): def nodeid(self):
@ -310,6 +391,24 @@ class Node(object):
repr_failure = _repr_failure_py repr_failure = _repr_failure_py
def get_fslocation_from_item(item):
"""Tries to extract the actual location from an item, depending on available attributes:
* "fslocation": a pair (path, lineno)
* "obj": a Python object that the item wraps.
* "fspath": just a path
:rtype: a tuple of (str|LocalPath, int) with filename and line number.
"""
result = getattr(item, "location", None)
if result is not None:
return result[:2]
obj = getattr(item, "obj", None)
if obj is not None:
return getfslineno(obj)
return getattr(item, "fspath", "unknown location"), -1
class Collector(Node): class Collector(Node):
""" Collector instances create children through collect() """ Collector instances create children through collect()
and thus iteratively build a tree. and thus iteratively build a tree.

View File

@ -126,7 +126,7 @@ class LsofFdLeakChecker(object):
error.append(error[0]) error.append(error[0])
error.append("*** function %s:%s: %s " % item.location) error.append("*** function %s:%s: %s " % item.location)
error.append("See issue #2366") error.append("See issue #2366")
item.warn("", "\n".join(error)) item.warn(pytest.PytestWarning("\n".join(error)))
# XXX copied from execnet's conftest.py - needs to be merged # XXX copied from execnet's conftest.py - needs to be merged
@ -525,7 +525,6 @@ class Testdir(object):
def make_hook_recorder(self, pluginmanager): def make_hook_recorder(self, pluginmanager):
"""Create a new :py:class:`HookRecorder` for a PluginManager.""" """Create a new :py:class:`HookRecorder` for a PluginManager."""
assert not hasattr(pluginmanager, "reprec")
pluginmanager.reprec = reprec = HookRecorder(pluginmanager) pluginmanager.reprec = reprec = HookRecorder(pluginmanager)
self.request.addfinalizer(reprec.finish_recording) self.request.addfinalizer(reprec.finish_recording)
return reprec return reprec
@ -643,10 +642,10 @@ class Testdir(object):
return p return p
def copy_example(self, name=None): def copy_example(self, name=None):
from . import experiments
import warnings import warnings
from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
warnings.warn(experiments.PYTESTER_COPY_EXAMPLE, stacklevel=2) warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2)
example_dir = self.request.config.getini("pytester_example_dir") example_dir = self.request.config.getini("pytester_example_dir")
if example_dir is None: if example_dir is None:
raise ValueError("pytester_example_dir is unset, can't copy examples") raise ValueError("pytester_example_dir is unset, can't copy examples")

View File

@ -44,7 +44,7 @@ from _pytest.mark.structures import (
get_unpacked_marks, get_unpacked_marks,
normalize_mark_list, normalize_mark_list,
) )
from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning
# relative paths that we use to filter traceback entries from appearing to the user; # relative paths that we use to filter traceback entries from appearing to the user;
# see filter_traceback # see filter_traceback
@ -239,9 +239,14 @@ def pytest_pycollect_makeitem(collector, name, obj):
# or a funtools.wrapped. # or a funtools.wrapped.
# We musn't if it's been wrapped with mock.patch (python 2 only) # 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 (isfunction(obj) or isfunction(get_real_func(obj))):
collector.warn( filename, lineno = getfslineno(obj)
code="C2", warnings.warn_explicit(
message="cannot collect %r because it is not a function." % name, message=PytestWarning(
"cannot collect %r because it is not a function." % name
),
category=None,
filename=str(filename),
lineno=lineno + 1,
) )
elif getattr(obj, "__test__", True): elif getattr(obj, "__test__", True):
if is_generator(obj): if is_generator(obj):
@ -349,11 +354,6 @@ class PyCollector(PyobjMixin, nodes.Collector):
if isinstance(obj, staticmethod): if isinstance(obj, staticmethod):
# static methods need to be unwrapped # static methods need to be unwrapped
obj = safe_getattr(obj, "__func__", False) obj = safe_getattr(obj, "__func__", False)
if obj is False:
# Python 2.6 wraps in a different way that we won't try to handle
msg = "cannot collect static method %r because it is not a function"
self.warn(code="C2", message=msg % name)
return False
return ( return (
safe_getattr(obj, "__call__", False) safe_getattr(obj, "__call__", False)
and fixtures.getfixturemarker(obj) is None and fixtures.getfixturemarker(obj) is None
@ -662,16 +662,18 @@ class Class(PyCollector):
return [] return []
if hasinit(self.obj): if hasinit(self.obj):
self.warn( self.warn(
"C1", PytestWarning(
"cannot collect test class %r because it has a " "cannot collect test class %r because it has a "
"__init__ constructor" % self.obj.__name__, "__init__ constructor" % self.obj.__name__
)
) )
return [] return []
elif hasnew(self.obj): elif hasnew(self.obj):
self.warn( self.warn(
"C1", PytestWarning(
"cannot collect test class %r because it has a " "cannot collect test class %r because it has a "
"__new__ constructor" % self.obj.__name__, "__new__ constructor" % self.obj.__name__
)
) )
return [] return []
return [self._getcustomclass("Instance")(name="()", parent=self)] return [self._getcustomclass("Instance")(name="()", parent=self)]
@ -799,7 +801,7 @@ class Generator(FunctionMixin, PyCollector):
) )
seen[name] = True seen[name] = True
values.append(self.Function(name, self, args=args, callobj=call)) values.append(self.Function(name, self, args=args, callobj=call))
self.warn("C1", deprecated.YIELD_TESTS) self.warn(deprecated.YIELD_TESTS)
return values return values
def getcallargs(self, obj): def getcallargs(self, obj):
@ -966,7 +968,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
argnames, parameters = ParameterSet._for_parametrize( argnames, parameters = ParameterSet._for_parametrize(
argnames, argvalues, self.function, self.config argnames,
argvalues,
self.function,
self.config,
function_definition=self.definition,
) )
del argvalues del argvalues
@ -977,7 +983,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
arg_values_types = self._resolve_arg_value_types(argnames, indirect) arg_values_types = self._resolve_arg_value_types(argnames, indirect)
ids = self._resolve_arg_ids(argnames, ids, parameters) ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
scopenum = scope2index(scope, descr="call to {}".format(self.parametrize)) scopenum = scope2index(scope, descr="call to {}".format(self.parametrize))
@ -1000,13 +1006,14 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
newcalls.append(newcallspec) newcalls.append(newcallspec)
self._calls = newcalls self._calls = newcalls
def _resolve_arg_ids(self, argnames, ids, parameters): def _resolve_arg_ids(self, argnames, ids, parameters, item):
"""Resolves the actual ids for the given argnames, based on the ``ids`` parameter given """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
to ``parametrize``. to ``parametrize``.
:param List[str] argnames: list of argument names passed to ``parametrize()``. :param List[str] argnames: list of argument names passed to ``parametrize()``.
:param ids: the ids parameter of the parametrized call (see docs). :param ids: the ids parameter of the parametrized call (see docs).
:param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``. :param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
:param Item item: the item that generated this parametrized call.
:rtype: List[str] :rtype: List[str]
:return: the list of ids for each argname given :return: the list of ids for each argname given
""" """
@ -1027,7 +1034,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
raise ValueError( raise ValueError(
msg % (saferepr(id_value), type(id_value).__name__) msg % (saferepr(id_value), type(id_value).__name__)
) )
ids = idmaker(argnames, parameters, idfn, ids, self.config) ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
return ids return ids
def _resolve_arg_value_types(self, argnames, indirect): def _resolve_arg_value_types(self, argnames, indirect):
@ -1100,10 +1107,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
:arg param: a parameter which will be exposed to a later fixture function :arg param: a parameter which will be exposed to a later fixture function
invocation through the ``request.param`` attribute. invocation through the ``request.param`` attribute.
""" """
if self.config: warnings.warn(deprecated.METAFUNC_ADD_CALL, stacklevel=2)
self.config.warn(
"C1", message=deprecated.METAFUNC_ADD_CALL, fslocation=None
)
assert funcargs is None or isinstance(funcargs, dict) assert funcargs is None or isinstance(funcargs, dict)
if funcargs is not None: if funcargs is not None:
for name in funcargs: for name in funcargs:
@ -1153,21 +1158,20 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
return "function" return "function"
def _idval(val, argname, idx, idfn, config=None): def _idval(val, argname, idx, idfn, item, config):
if idfn: if idfn:
s = None s = None
try: try:
s = idfn(val) s = idfn(val)
except Exception: except Exception as e:
# See issue https://github.com/pytest-dev/pytest/issues/2169 # See issue https://github.com/pytest-dev/pytest/issues/2169
import warnings
msg = ( msg = (
"Raised while trying to determine id of parameter %s at position %d." "While trying to determine id of parameter {} at position "
% (argname, idx) "{} the following exception was raised:\n".format(argname, idx)
) )
msg += "\nUpdate your code as this will raise an error in pytest-4.0." msg += " {}: {}\n".format(type(e).__name__, e)
warnings.warn(msg, DeprecationWarning) msg += "This warning will be an error error in pytest-4.0."
item.warn(RemovedInPytest4Warning(msg))
if s: if s:
return ascii_escaped(s) return ascii_escaped(s)
@ -1191,12 +1195,12 @@ def _idval(val, argname, idx, idfn, config=None):
return str(argname) + str(idx) return str(argname) + str(idx)
def _idvalset(idx, parameterset, argnames, idfn, ids, config=None): def _idvalset(idx, parameterset, argnames, idfn, ids, item, config):
if parameterset.id is not None: if parameterset.id is not None:
return parameterset.id return parameterset.id
if ids is None or (idx >= len(ids) or ids[idx] is None): if ids is None or (idx >= len(ids) or ids[idx] is None):
this_id = [ this_id = [
_idval(val, argname, idx, idfn, config) _idval(val, argname, idx, idfn, item=item, config=config)
for val, argname in zip(parameterset.values, argnames) for val, argname in zip(parameterset.values, argnames)
] ]
return "-".join(this_id) return "-".join(this_id)
@ -1204,9 +1208,9 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
return ascii_escaped(ids[idx]) return ascii_escaped(ids[idx])
def idmaker(argnames, parametersets, idfn=None, ids=None, config=None): def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None):
ids = [ ids = [
_idvalset(valindex, parameterset, argnames, idfn, ids, config) _idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item)
for valindex, parameterset in enumerate(parametersets) for valindex, parameterset in enumerate(parametersets)
] ]
if len(set(ids)) != len(ids): if len(set(ids)) != len(ids):

View File

@ -31,8 +31,10 @@ def pytest_configure(config):
config.pluginmanager.register(config._resultlog) config.pluginmanager.register(config._resultlog)
from _pytest.deprecated import RESULT_LOG from _pytest.deprecated import RESULT_LOG
from _pytest.warning_types import RemovedInPytest4Warning
from _pytest.warnings import _issue_config_warning
config.warn("C1", RESULT_LOG) _issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config)
def pytest_unconfigure(config): def pytest_unconfigure(config):

View File

@ -9,6 +9,7 @@ import platform
import sys import sys
import time import time
import attr
import pluggy import pluggy
import py import py
import six import six
@ -184,23 +185,23 @@ def pytest_report_teststatus(report):
return report.outcome, letter, report.outcome.upper() return report.outcome, letter, report.outcome.upper()
@attr.s
class WarningReport(object): class WarningReport(object):
""" """
Simple structure to hold warnings information captured by ``pytest_logwarning``. Simple structure to hold warnings information captured by ``pytest_logwarning`` and ``pytest_warning_captured``.
:ivar str message: user friendly message about the warning
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
:ivar tuple|py.path.local fslocation:
file system location of the source of the warning (see ``get_location``).
:ivar bool legacy: if this warning report was generated from the deprecated ``pytest_logwarning`` hook.
""" """
def __init__(self, code, message, nodeid=None, fslocation=None): message = attr.ib()
""" nodeid = attr.ib(default=None)
:param code: unused fslocation = attr.ib(default=None)
:param str message: user friendly message about the warning legacy = attr.ib(default=False)
:param str|None nodeid: node id that generated the warning (see ``get_location``).
:param tuple|py.path.local fslocation:
file system location of the source of the warning (see ``get_location``).
"""
self.code = code
self.message = message
self.nodeid = nodeid
self.fslocation = fslocation
def get_location(self, config): def get_location(self, config):
""" """
@ -213,6 +214,8 @@ class WarningReport(object):
if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
filename, linenum = self.fslocation[:2] filename, linenum = self.fslocation[:2]
relpath = py.path.local(filename).relto(config.invocation_dir) relpath = py.path.local(filename).relto(config.invocation_dir)
if not relpath:
relpath = str(filename)
return "%s:%s" % (relpath, linenum) return "%s:%s" % (relpath, linenum)
else: else:
return str(self.fslocation) return str(self.fslocation)
@ -327,13 +330,27 @@ class TerminalReporter(object):
self.write_line("INTERNALERROR> " + line) self.write_line("INTERNALERROR> " + line)
return 1 return 1
def pytest_logwarning(self, code, fslocation, message, nodeid): def pytest_logwarning(self, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", []) warnings = self.stats.setdefault("warnings", [])
warning = WarningReport( warning = WarningReport(
code=code, fslocation=fslocation, message=message, nodeid=nodeid fslocation=fslocation, message=message, nodeid=nodeid, legacy=True
) )
warnings.append(warning) warnings.append(warning)
def pytest_warning_captured(self, warning_message, item):
# from _pytest.nodes import get_fslocation_from_item
from _pytest.warnings import warning_record_to_str
warnings = self.stats.setdefault("warnings", [])
fslocation = warning_message.filename, warning_message.lineno
message = warning_record_to_str(warning_message)
nodeid = item.nodeid if item is not None else ""
warning_report = WarningReport(
fslocation=fslocation, message=message, nodeid=nodeid
)
warnings.append(warning_report)
def pytest_plugin_registered(self, plugin): def pytest_plugin_registered(self, plugin):
if self.config.option.traceconfig: if self.config.option.traceconfig:
msg = "PLUGIN registered: %s" % (plugin,) msg = "PLUGIN registered: %s" % (plugin,)
@ -697,11 +714,20 @@ class TerminalReporter(object):
self.write_sep("=", "warnings summary", yellow=True, bold=False) self.write_sep("=", "warnings summary", yellow=True, bold=False)
for location, warning_records in grouped: for location, warning_records in grouped:
self._tw.line(str(location) if location else "<undetermined location>") # legacy warnings show their location explicitly, while standard warnings look better without
# it because the location is already formatted into the message
warning_records = list(warning_records)
is_legacy = warning_records[0].legacy
if location and is_legacy:
self._tw.line(str(location))
for w in warning_records: for w in warning_records:
lines = w.message.splitlines() if is_legacy:
indented = "\n".join(" " + x for x in lines) lines = w.message.splitlines()
self._tw.line(indented) indented = "\n".join(" " + x for x in lines)
message = indented.rstrip()
else:
message = w.message.rstrip()
self._tw.line(message)
self._tw.line() self._tw.line()
self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html")

View File

@ -0,0 +1,42 @@
class PytestWarning(UserWarning):
"""
Bases: :class:`UserWarning`.
Base class for all warnings emitted by pytest.
"""
class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
"""
Bases: :class:`pytest.PytestWarning`, :class:`DeprecationWarning`.
Warning class for features that will be removed in a future version.
"""
class RemovedInPytest4Warning(PytestDeprecationWarning):
"""
Bases: :class:`pytest.PytestDeprecationWarning`.
Warning class for features scheduled to be removed in pytest 4.0.
"""
class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
"""
Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`.
Warning category used to denote experiments in pytest. Use sparingly as the API might change or even be
removed completely in future version
"""
@classmethod
def simple(cls, apiname):
return cls(
"{apiname} is an experimental api that may change over time".format(
apiname=apiname
)
)
PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example")

View File

@ -1,5 +1,6 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys
import warnings import warnings
from contextlib import contextmanager from contextlib import contextmanager
@ -58,62 +59,114 @@ def pytest_configure(config):
@contextmanager @contextmanager
def catch_warnings_for_item(item): def catch_warnings_for_item(config, ihook, when, item):
""" """
catches the warnings generated during setup/call/teardown execution Context manager that catches warnings generated in the contained execution block.
of the given item and after it is done posts them as warnings to this
item. ``item`` can be None if we are not in the context of an item execution.
Each warning captured triggers the ``pytest_warning_captured`` hook.
""" """
args = item.config.getoption("pythonwarnings") or [] args = config.getoption("pythonwarnings") or []
inifilters = item.config.getini("filterwarnings") inifilters = config.getini("filterwarnings")
with warnings.catch_warnings(record=True) as log: with warnings.catch_warnings(record=True) as log:
filters_configured = args or inifilters or sys.warnoptions
for arg in args: for arg in args:
warnings._setoption(arg) warnings._setoption(arg)
for arg in inifilters: for arg in inifilters:
_setoption(warnings, arg) _setoption(warnings, arg)
for mark in item.iter_markers(name="filterwarnings"): if item is not None:
for arg in mark.args: for mark in item.iter_markers(name="filterwarnings"):
warnings._setoption(arg) for arg in mark.args:
_setoption(warnings, arg)
filters_configured = True
if not filters_configured:
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
warnings.filterwarnings("always", category=DeprecationWarning)
warnings.filterwarnings("always", category=PendingDeprecationWarning)
yield yield
for warning in log: for warning_message in log:
warn_msg = warning.message ihook.pytest_warning_captured.call_historic(
unicode_warning = False kwargs=dict(warning_message=warning_message, when=when, item=item)
if compat._PY2 and any(
isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args
):
new_args = []
for m in warn_msg.args:
new_args.append(
compat.ascii_escaped(m)
if isinstance(m, compat.UNICODE_TYPES)
else m
)
unicode_warning = list(warn_msg.args) != new_args
warn_msg.args = new_args
msg = warnings.formatwarning(
warn_msg,
warning.category,
warning.filename,
warning.lineno,
warning.line,
) )
item.warn("unused", msg)
if unicode_warning:
warnings.warn( def warning_record_to_str(warning_message):
"Warning is using unicode non convertible to ascii, " """Convert a warnings.WarningMessage to a string, taking in account a lot of unicode shenaningans in Python 2.
"converting to a safe representation:\n %s" % msg,
UnicodeWarning, When Python 2 support is dropped this function can be greatly simplified.
) """
warn_msg = warning_message.message
unicode_warning = False
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
new_args = []
for m in warn_msg.args:
new_args.append(
compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m
)
unicode_warning = list(warn_msg.args) != new_args
warn_msg.args = new_args
msg = warnings.formatwarning(
warn_msg,
warning_message.category,
warning_message.filename,
warning_message.lineno,
warning_message.line,
)
if unicode_warning:
warnings.warn(
"Warning is using unicode non convertible to ascii, "
"converting to a safe representation:\n %s" % msg,
UnicodeWarning,
)
return msg
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_protocol(item):
with catch_warnings_for_item(
config=item.config, ihook=item.ihook, when="runtest", item=item
):
yield
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection(session):
config = session.config
with catch_warnings_for_item(
config=config, ihook=config.hook, when="collect", item=None
):
yield
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item): def pytest_terminal_summary(terminalreporter):
with catch_warnings_for_item(item): config = terminalreporter.config
with catch_warnings_for_item(
config=config, ihook=config.hook, when="config", item=None
):
yield yield
def _issue_config_warning(warning, config):
"""
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891.
:param warning: the warning instance.
:param config:
"""
with warnings.catch_warnings(record=True) as records:
warnings.simplefilter("always", type(warning))
warnings.warn(warning, stacklevel=2)
config.hook.pytest_warning_captured.call_historic(
kwargs=dict(warning_message=records[0], when="config", item=None)
)

View File

@ -19,45 +19,54 @@ from _pytest.main import Session
from _pytest.nodes import Item, Collector, File from _pytest.nodes import Item, Collector, File
from _pytest.fixtures import fillfixtures as _fillfuncargs from _pytest.fixtures import fillfixtures as _fillfuncargs
from _pytest.python import Package, Module, Class, Instance, Function, Generator from _pytest.python import Package, Module, Class, Instance, Function, Generator
from _pytest.python_api import approx, raises from _pytest.python_api import approx, raises
from _pytest.warning_types import (
PytestWarning,
PytestDeprecationWarning,
RemovedInPytest4Warning,
PytestExperimentalApiWarning,
)
set_trace = __pytestPDB.set_trace set_trace = __pytestPDB.set_trace
__all__ = [ __all__ = [
"main",
"UsageError",
"cmdline",
"hookspec",
"hookimpl",
"__version__", "__version__",
"register_assert_rewrite",
"freeze_includes",
"set_trace",
"warns",
"deprecated_call",
"fixture",
"yield_fixture",
"fail",
"skip",
"xfail",
"importorskip",
"exit",
"mark",
"param",
"approx",
"_fillfuncargs", "_fillfuncargs",
"Item", "approx",
"File",
"Collector",
"Package",
"Session",
"Module",
"Class", "Class",
"Instance", "cmdline",
"Collector",
"deprecated_call",
"exit",
"fail",
"File",
"fixture",
"freeze_includes",
"Function", "Function",
"Generator", "Generator",
"hookimpl",
"hookspec",
"importorskip",
"Instance",
"Item",
"main",
"mark",
"Module",
"Package",
"param",
"PytestDeprecationWarning",
"PytestExperimentalApiWarning",
"PytestWarning",
"raises", "raises",
"register_assert_rewrite",
"RemovedInPytest4Warning",
"Session",
"set_trace",
"skip",
"UsageError",
"warns",
"xfail",
"yield_fixture",
] ]
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,9 +1,11 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import os
import pytest import pytest
@pytest.mark.filterwarnings("default")
def test_yield_tests_deprecation(testdir): def test_yield_tests_deprecation(testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
@ -17,16 +19,18 @@ def test_yield_tests_deprecation(testdir):
yield func1, 1, 1 yield func1, 1, 1
""" """
) )
result = testdir.runpytest("-ra") result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*yield tests are deprecated, and scheduled to be removed in pytest 4.0*", "*test_yield_tests_deprecation.py:3:*yield tests are deprecated*",
"*test_yield_tests_deprecation.py:6:*yield tests are deprecated*",
"*2 passed*", "*2 passed*",
] ]
) )
assert result.stdout.str().count("yield tests are deprecated") == 2 assert result.stdout.str().count("yield tests are deprecated") == 2
@pytest.mark.filterwarnings("default")
def test_funcarg_prefix_deprecation(testdir): def test_funcarg_prefix_deprecation(testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
@ -41,16 +45,15 @@ def test_funcarg_prefix_deprecation(testdir):
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
( (
"*pytest_funcarg__value: " "*test_funcarg_prefix_deprecation.py:1: *pytest_funcarg__value: "
'declaring fixtures using "pytest_funcarg__" prefix is deprecated ' 'declaring fixtures using "pytest_funcarg__" prefix is deprecated*'
"and scheduled to be removed in pytest 4.0. "
"Please remove the prefix and use the @pytest.fixture decorator instead."
), ),
"*1 passed*", "*1 passed*",
] ]
) )
@pytest.mark.filterwarnings("default")
def test_pytest_setup_cfg_deprecated(testdir): def test_pytest_setup_cfg_deprecated(testdir):
testdir.makefile( testdir.makefile(
".cfg", ".cfg",
@ -65,6 +68,7 @@ def test_pytest_setup_cfg_deprecated(testdir):
) )
@pytest.mark.filterwarnings("default")
def test_pytest_custom_cfg_deprecated(testdir): def test_pytest_custom_cfg_deprecated(testdir):
testdir.makefile( testdir.makefile(
".cfg", ".cfg",
@ -79,15 +83,15 @@ def test_pytest_custom_cfg_deprecated(testdir):
) )
def test_str_args_deprecated(tmpdir, testdir): def test_str_args_deprecated(tmpdir):
"""Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0.""" """Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0."""
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
warnings = [] warnings = []
class Collect(object): class Collect(object):
def pytest_logwarning(self, message): def pytest_warning_captured(self, warning_message):
warnings.append(message) warnings.append(str(warning_message.message))
ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()])
msg = ( msg = (
@ -102,6 +106,7 @@ def test_getfuncargvalue_is_deprecated(request):
pytest.deprecated_call(request.getfuncargvalue, "tmpdir") pytest.deprecated_call(request.getfuncargvalue, "tmpdir")
@pytest.mark.filterwarnings("default")
def test_resultlog_is_deprecated(testdir): def test_resultlog_is_deprecated(testdir):
result = testdir.runpytest("--help") result = testdir.runpytest("--help")
result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"]) result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"])
@ -197,8 +202,11 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir):
) )
res = testdir.runpytest_subprocess() res = testdir.runpytest_subprocess()
assert res.ret == 0 assert res.ret == 0
res.stderr.fnmatch_lines( msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
"*" + str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] res.stdout.fnmatch_lines(
"*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format(
sep=os.sep, msg=msg
)
) )
@ -227,8 +235,11 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_confte
res = testdir.runpytest_subprocess() res = testdir.runpytest_subprocess()
assert res.ret == 0 assert res.ret == 0
res.stderr.fnmatch_lines( msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
"*" + str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] res.stdout.fnmatch_lines(
"*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format(
sep=os.sep, msg=msg
)
) )
@ -261,10 +272,8 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives(
) )
res = testdir.runpytest_subprocess() res = testdir.runpytest_subprocess()
assert res.ret == 0 assert res.ret == 0
assert ( msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] assert msg not in res.stdout.str()
not in res.stderr.str()
)
def test_call_fixture_function_deprecated(): def test_call_fixture_function_deprecated():
@ -276,3 +285,23 @@ def test_call_fixture_function_deprecated():
with pytest.deprecated_call(): with pytest.deprecated_call():
assert fix() == 1 assert fix() == 1
def test_pycollector_makeitem_is_deprecated():
from _pytest.python import PyCollector
from _pytest.warning_types import RemovedInPytest4Warning
class PyCollectorMock(PyCollector):
"""evil hack"""
def __init__(self):
self.called = False
def _makeitem(self, *k):
"""hack to disable the actual behaviour"""
self.called = True
collector = PyCollectorMock()
with pytest.warns(RemovedInPytest4Warning):
collector.makeitem("foo", "bar")
assert collector.called

View File

@ -8,10 +8,6 @@ import pytest
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.nodes import Collector from _pytest.nodes import Collector
ignore_parametrized_marks = pytest.mark.filterwarnings(
"ignore:Applying marks directly to parameters"
)
class TestModule(object): class TestModule(object):
def test_failing_import(self, testdir): def test_failing_import(self, testdir):
@ -456,12 +452,20 @@ class TestGenerator(object):
class TestFunction(object): class TestFunction(object):
@pytest.fixture
def ignore_parametrized_marks_args(self):
"""Provides arguments to pytester.runpytest() to ignore the warning about marks being applied directly
to parameters.
"""
return ("-W", "ignore:Applying marks directly to parameters")
def test_getmodulecollector(self, testdir): def test_getmodulecollector(self, testdir):
item = testdir.getitem("def test_func(): pass") item = testdir.getitem("def test_func(): pass")
modcol = item.getparent(pytest.Module) modcol = item.getparent(pytest.Module)
assert isinstance(modcol, pytest.Module) assert isinstance(modcol, pytest.Module)
assert hasattr(modcol.obj, "test_func") assert hasattr(modcol.obj, "test_func")
@pytest.mark.filterwarnings("default")
def test_function_as_object_instance_ignored(self, testdir): def test_function_as_object_instance_ignored(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
@ -472,8 +476,14 @@ class TestFunction(object):
test_a = A() test_a = A()
""" """
) )
reprec = testdir.inline_run() result = testdir.runpytest()
reprec.assertoutcome() result.stdout.fnmatch_lines(
[
"collected 0 items",
"*test_function_as_object_instance_ignored.py:2: "
"*cannot collect 'test_a' because it is not a function.",
]
)
def test_function_equality(self, testdir, tmpdir): def test_function_equality(self, testdir, tmpdir):
from _pytest.fixtures import FixtureManager from _pytest.fixtures import FixtureManager
@ -662,7 +672,7 @@ class TestFunction(object):
rec = testdir.inline_run() rec = testdir.inline_run()
rec.assertoutcome(passed=1) rec.assertoutcome(passed=1)
@ignore_parametrized_marks @pytest.mark.filterwarnings("ignore:Applying marks directly to parameters")
def test_parametrize_with_mark(self, testdir): def test_parametrize_with_mark(self, testdir):
items = testdir.getitems( items = testdir.getitems(
""" """
@ -748,8 +758,7 @@ class TestFunction(object):
assert colitems[2].name == "test2[a-c]" assert colitems[2].name == "test2[a-c]"
assert colitems[3].name == "test2[b-c]" assert colitems[3].name == "test2[b-c]"
@ignore_parametrized_marks def test_parametrize_skipif(self, testdir, ignore_parametrized_marks_args):
def test_parametrize_skipif(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -761,11 +770,10 @@ class TestFunction(object):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest() result = testdir.runpytest(*ignore_parametrized_marks_args)
result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *")
@ignore_parametrized_marks def test_parametrize_skip(self, testdir, ignore_parametrized_marks_args):
def test_parametrize_skip(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -777,11 +785,10 @@ class TestFunction(object):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest() result = testdir.runpytest(*ignore_parametrized_marks_args)
result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *")
@ignore_parametrized_marks def test_parametrize_skipif_no_skip(self, testdir, ignore_parametrized_marks_args):
def test_parametrize_skipif_no_skip(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -793,11 +800,10 @@ class TestFunction(object):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest() result = testdir.runpytest(*ignore_parametrized_marks_args)
result.stdout.fnmatch_lines("* 1 failed, 2 passed in *") result.stdout.fnmatch_lines("* 1 failed, 2 passed in *")
@ignore_parametrized_marks def test_parametrize_xfail(self, testdir, ignore_parametrized_marks_args):
def test_parametrize_xfail(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -809,11 +815,10 @@ class TestFunction(object):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest() result = testdir.runpytest(*ignore_parametrized_marks_args)
result.stdout.fnmatch_lines("* 2 passed, 1 xfailed in *") result.stdout.fnmatch_lines("* 2 passed, 1 xfailed in *")
@ignore_parametrized_marks def test_parametrize_passed(self, testdir, ignore_parametrized_marks_args):
def test_parametrize_passed(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -825,11 +830,10 @@ class TestFunction(object):
pass pass
""" """
) )
result = testdir.runpytest() result = testdir.runpytest(*ignore_parametrized_marks_args)
result.stdout.fnmatch_lines("* 2 passed, 1 xpassed in *") result.stdout.fnmatch_lines("* 2 passed, 1 xpassed in *")
@ignore_parametrized_marks def test_parametrize_xfail_passed(self, testdir, ignore_parametrized_marks_args):
def test_parametrize_xfail_passed(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -841,7 +845,7 @@ class TestFunction(object):
pass pass
""" """
) )
result = testdir.runpytest() result = testdir.runpytest(*ignore_parametrized_marks_args)
result.stdout.fnmatch_lines("* 3 passed in *") result.stdout.fnmatch_lines("* 3 passed in *")
def test_function_original_name(self, testdir): def test_function_original_name(self, testdir):
@ -1468,6 +1472,7 @@ def test_collect_functools_partial(testdir):
result.assertoutcome(passed=6, failed=2) result.assertoutcome(passed=6, failed=2)
@pytest.mark.filterwarnings("default")
def test_dont_collect_non_function_callable(testdir): def test_dont_collect_non_function_callable(testdir):
"""Test for issue https://github.com/pytest-dev/pytest/issues/331 """Test for issue https://github.com/pytest-dev/pytest/issues/331
@ -1490,7 +1495,7 @@ def test_dont_collect_non_function_callable(testdir):
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*collected 1 item*", "*collected 1 item*",
"*cannot collect 'test_a' because it is not a function*", "*test_dont_collect_non_function_callable.py:2: *cannot collect 'test_a' because it is not a function*",
"*1 passed, 1 warnings in *", "*1 passed, 1 warnings in *",
] ]
) )

View File

@ -217,7 +217,7 @@ class TestMetafunc(object):
def test_idval_hypothesis(self, value): def test_idval_hypothesis(self, value):
from _pytest.python import _idval from _pytest.python import _idval
escaped = _idval(value, "a", 6, None) escaped = _idval(value, "a", 6, None, item=None, config=None)
assert isinstance(escaped, str) assert isinstance(escaped, str)
if PY3: if PY3:
escaped.encode("ascii") escaped.encode("ascii")
@ -244,7 +244,7 @@ class TestMetafunc(object):
), ),
] ]
for val, expected in values: for val, expected in values:
assert _idval(val, "a", 6, None) == expected assert _idval(val, "a", 6, None, item=None, config=None) == expected
def test_bytes_idval(self): def test_bytes_idval(self):
"""unittest for the expected behavior to obtain ids for parametrized """unittest for the expected behavior to obtain ids for parametrized
@ -262,7 +262,7 @@ class TestMetafunc(object):
(u"αρά".encode("utf-8"), "\\xce\\xb1\\xcf\\x81\\xce\\xac"), (u"αρά".encode("utf-8"), "\\xce\\xb1\\xcf\\x81\\xce\\xac"),
] ]
for val, expected in values: for val, expected in values:
assert _idval(val, "a", 6, None) == expected assert _idval(val, "a", 6, idfn=None, item=None, config=None) == expected
def test_class_or_function_idval(self): def test_class_or_function_idval(self):
"""unittest for the expected behavior to obtain ids for parametrized """unittest for the expected behavior to obtain ids for parametrized
@ -278,7 +278,7 @@ class TestMetafunc(object):
values = [(TestClass, "TestClass"), (test_function, "test_function")] values = [(TestClass, "TestClass"), (test_function, "test_function")]
for val, expected in values: for val, expected in values:
assert _idval(val, "a", 6, None) == expected assert _idval(val, "a", 6, None, item=None, config=None) == expected
@pytest.mark.issue250 @pytest.mark.issue250
def test_idmaker_autoname(self): def test_idmaker_autoname(self):
@ -383,44 +383,7 @@ class TestMetafunc(object):
) )
assert result == ["a-a0", "a-a1", "a-a2"] assert result == ["a-a0", "a-a1", "a-a2"]
@pytest.mark.issue351 @pytest.mark.filterwarnings("default")
def test_idmaker_idfn_exception(self):
from _pytest.python import idmaker
from _pytest.recwarn import WarningsRecorder
class BadIdsException(Exception):
pass
def ids(val):
raise BadIdsException("ids raised")
rec = WarningsRecorder()
with rec:
idmaker(
("a", "b"),
[
pytest.param(10.0, IndexError()),
pytest.param(20, KeyError()),
pytest.param("three", [1, 2, 3]),
],
idfn=ids,
)
assert [str(i.message) for i in rec.list] == [
"Raised while trying to determine id of parameter a at position 0."
"\nUpdate your code as this will raise an error in pytest-4.0.",
"Raised while trying to determine id of parameter b at position 0."
"\nUpdate your code as this will raise an error in pytest-4.0.",
"Raised while trying to determine id of parameter a at position 1."
"\nUpdate your code as this will raise an error in pytest-4.0.",
"Raised while trying to determine id of parameter b at position 1."
"\nUpdate your code as this will raise an error in pytest-4.0.",
"Raised while trying to determine id of parameter a at position 2."
"\nUpdate your code as this will raise an error in pytest-4.0.",
"Raised while trying to determine id of parameter b at position 2."
"\nUpdate your code as this will raise an error in pytest-4.0.",
]
def test_parametrize_ids_exception(self, testdir): def test_parametrize_ids_exception(self, testdir):
""" """
:param testdir: the instance of Testdir class, a temporary :param testdir: the instance of Testdir class, a temporary
@ -438,13 +401,14 @@ class TestMetafunc(object):
pass pass
""" """
) )
with pytest.warns(DeprecationWarning): result = testdir.runpytest("--collect-only")
result = testdir.runpytest("--collect-only")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"<Module 'test_parametrize_ids_exception.py'>", "<Module 'test_parametrize_ids_exception.py'>",
" <Function 'test_foo[a]'>", " <Function 'test_foo[a]'>",
" <Function 'test_foo[b]'>", " <Function 'test_foo[b]'>",
"*test_parametrize_ids_exception.py:6: *parameter arg at position 0*",
"*test_parametrize_ids_exception.py:6: *parameter arg at position 1*",
] ]
) )

View File

@ -1,22 +0,0 @@
import pytest
from _pytest.python import PyCollector
class PyCollectorMock(PyCollector):
"""evil hack"""
def __init__(self):
self.called = False
def _makeitem(self, *k):
"""hack to disable the actual behaviour"""
self.called = True
def test_pycollector_makeitem_is_deprecated():
collector = PyCollectorMock()
with pytest.deprecated_call():
collector.makeitem("foo", "bar")
assert collector.called

View File

@ -1075,17 +1075,27 @@ def test_diff_newline_at_end(monkeypatch, testdir):
) )
@pytest.mark.filterwarnings("default")
def test_assert_tuple_warning(testdir): def test_assert_tuple_warning(testdir):
msg = "assertion is always true"
testdir.makepyfile( testdir.makepyfile(
""" """
def test_tuple(): def test_tuple():
assert(False, 'you shall not pass') assert(False, 'you shall not pass')
""" """
) )
result = testdir.runpytest("-rw") result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(["*test_assert_tuple_warning.py:2:*{}*".format(msg)])
["*test_assert_tuple_warning.py:2", "*assertion is always true*"]
# tuples with size != 2 should not trigger the warning
testdir.makepyfile(
"""
def test_tuple():
assert ()
"""
) )
result = testdir.runpytest()
assert msg not in result.stdout.str()
def test_assert_indirect_tuple_no_warning(testdir): def test_assert_indirect_tuple_no_warning(testdir):

View File

@ -759,16 +759,16 @@ def test_rewritten():
testdir.makepyfile("import a_package_without_init_py.module") testdir.makepyfile("import a_package_without_init_py.module")
assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED
def test_rewrite_warning(self, pytestconfig, monkeypatch): def test_rewrite_warning(self, testdir):
hook = AssertionRewritingHook(pytestconfig) testdir.makeconftest(
warnings = [] """
import pytest
def mywarn(code, msg): pytest.register_assert_rewrite("_pytest")
warnings.append((code, msg)) """
)
monkeypatch.setattr(hook.config, "warn", mywarn) # needs to be a subprocess because pytester explicitly disables this warning
hook.mark_rewrite("_pytest") result = testdir.runpytest_subprocess()
assert "_pytest" in warnings[0][1] result.stdout.fnmatch_lines("*Module already imported*: _pytest")
def test_rewrite_module_imported_from_conftest(self, testdir): def test_rewrite_module_imported_from_conftest(self, testdir):
testdir.makeconftest( testdir.makeconftest(

View File

@ -31,6 +31,7 @@ class TestNewAPI(object):
val = config.cache.get("key/name", -2) val = config.cache.get("key/name", -2)
assert val == -2 assert val == -2
@pytest.mark.filterwarnings("default")
def test_cache_writefail_cachfile_silent(self, testdir): def test_cache_writefail_cachfile_silent(self, testdir):
testdir.makeini("[pytest]") testdir.makeini("[pytest]")
testdir.tmpdir.join(".pytest_cache").write("gone wrong") testdir.tmpdir.join(".pytest_cache").write("gone wrong")
@ -39,6 +40,9 @@ class TestNewAPI(object):
cache.set("test/broken", []) cache.set("test/broken", [])
@pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows")
@pytest.mark.filterwarnings(
"ignore:could not create cache path:pytest.PytestWarning"
)
def test_cache_writefail_permissions(self, testdir): def test_cache_writefail_permissions(self, testdir):
testdir.makeini("[pytest]") testdir.makeini("[pytest]")
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0)
@ -47,6 +51,7 @@ class TestNewAPI(object):
cache.set("test/broken", []) cache.set("test/broken", [])
@pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows")
@pytest.mark.filterwarnings("default")
def test_cache_failure_warns(self, testdir): def test_cache_failure_warns(self, testdir):
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0)
testdir.makepyfile( testdir.makepyfile(

View File

@ -18,7 +18,9 @@ from _pytest.capture import CaptureManager
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
needsosdup = pytest.mark.xfail("not hasattr(os, 'dup')") needsosdup = pytest.mark.skipif(
not hasattr(os, "dup"), reason="test needs os.dup, not available on this platform"
)
def tobytes(obj): def tobytes(obj):
@ -61,9 +63,8 @@ class TestCaptureManager(object):
pytest_addoption(parser) pytest_addoption(parser)
assert parser._groups[0].options[0].default == "sys" assert parser._groups[0].options[0].default == "sys"
@needsosdup
@pytest.mark.parametrize( @pytest.mark.parametrize(
"method", ["no", "sys", pytest.mark.skipif('not hasattr(os, "dup")', "fd")] "method", ["no", "sys", pytest.param("fd", marks=needsosdup)]
) )
def test_capturing_basic_api(self, method): def test_capturing_basic_api(self, method):
capouter = StdCaptureFD() capouter = StdCaptureFD()

View File

@ -135,13 +135,13 @@ class TestConfigCmdlineParsing(object):
""" """
) )
testdir.makefile( testdir.makefile(
".cfg", ".ini",
custom=""" custom="""
[pytest] [pytest]
custom = 1 custom = 1
""", """,
) )
config = testdir.parseconfig("-c", "custom.cfg") config = testdir.parseconfig("-c", "custom.ini")
assert config.getini("custom") == "1" assert config.getini("custom") == "1"
testdir.makefile( testdir.makefile(
@ -155,8 +155,8 @@ class TestConfigCmdlineParsing(object):
assert config.getini("custom") == "1" assert config.getini("custom") == "1"
def test_absolute_win32_path(self, testdir): def test_absolute_win32_path(self, testdir):
temp_cfg_file = testdir.makefile( temp_ini_file = testdir.makefile(
".cfg", ".ini",
custom=""" custom="""
[pytest] [pytest]
addopts = --version addopts = --version
@ -164,8 +164,8 @@ class TestConfigCmdlineParsing(object):
) )
from os.path import normpath from os.path import normpath
temp_cfg_file = normpath(str(temp_cfg_file)) temp_ini_file = normpath(str(temp_ini_file))
ret = pytest.main("-c " + temp_cfg_file) ret = pytest.main(["-c", temp_ini_file])
assert ret == _pytest.main.EXIT_OK assert ret == _pytest.main.EXIT_OK
@ -783,13 +783,14 @@ def test_collect_pytest_prefix_bug(pytestconfig):
assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None
class TestWarning(object): class TestLegacyWarning(object):
@pytest.mark.filterwarnings("default")
def test_warn_config(self, testdir): def test_warn_config(self, testdir):
testdir.makeconftest( testdir.makeconftest(
""" """
values = [] values = []
def pytest_configure(config): def pytest_runtest_setup(item):
config.warn("C1", "hello") item.config.warn("C1", "hello")
def pytest_logwarning(code, message): def pytest_logwarning(code, message):
if message == "hello" and code == "C1": if message == "hello" and code == "C1":
values.append(1) values.append(1)
@ -802,24 +803,31 @@ class TestWarning(object):
assert conftest.values == [1] assert conftest.values == [1]
""" """
) )
reprec = testdir.inline_run() result = testdir.runpytest()
reprec.assertoutcome(passed=1) result.stdout.fnmatch_lines(
["*hello", "*config.warn has been deprecated*", "*1 passed*"]
)
def test_warn_on_test_item_from_request(self, testdir, request): @pytest.mark.filterwarnings("default")
@pytest.mark.parametrize("use_kw", [True, False])
def test_warn_on_test_item_from_request(self, testdir, use_kw):
code_kw = "code=" if use_kw else ""
message_kw = "message=" if use_kw else ""
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@pytest.fixture @pytest.fixture
def fix(request): def fix(request):
request.node.warn("T1", "hello") request.node.warn({code_kw}"T1", {message_kw}"hello")
def test_hello(fix): def test_hello(fix):
pass pass
""" """.format(
code_kw=code_kw, message_kw=message_kw
)
) )
result = testdir.runpytest("--disable-pytest-warnings") result = testdir.runpytest("--disable-pytest-warnings")
assert result.parseoutcomes()["warnings"] > 0
assert "hello" not in result.stdout.str() assert "hello" not in result.stdout.str()
result = testdir.runpytest() result = testdir.runpytest()
@ -828,6 +836,7 @@ class TestWarning(object):
===*warnings summary*=== ===*warnings summary*===
*test_warn_on_test_item_from_request.py::test_hello* *test_warn_on_test_item_from_request.py::test_hello*
*hello* *hello*
*test_warn_on_test_item_from_request.py:7:*Node.warn(code, message) form has been deprecated*
""" """
) )
@ -847,7 +856,7 @@ class TestRootdir(object):
@pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split())
def test_with_ini(self, tmpdir, name): def test_with_ini(self, tmpdir, name):
inifile = tmpdir.join(name) inifile = tmpdir.join(name)
inifile.write("[pytest]\n") inifile.write("[pytest]\n" if name != "setup.cfg" else "[tool:pytest]\n")
a = tmpdir.mkdir("a") a = tmpdir.mkdir("a")
b = a.mkdir("b") b = a.mkdir("b")
@ -893,11 +902,14 @@ class TestRootdir(object):
class TestOverrideIniArgs(object): class TestOverrideIniArgs(object):
@pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split())
def test_override_ini_names(self, testdir, name): def test_override_ini_names(self, testdir, name):
section = "[pytest]" if name != "setup.cfg" else "[tool:pytest]"
testdir.tmpdir.join(name).write( testdir.tmpdir.join(name).write(
textwrap.dedent( textwrap.dedent(
""" """
[pytest] {section}
custom = 1.0""" custom = 1.0""".format(
section=section
)
) )
) )
testdir.makeconftest( testdir.makeconftest(

View File

@ -1005,6 +1005,7 @@ def test_record_property_same_name(testdir):
pnodes[1].assert_attr(name="foo", value="baz") pnodes[1].assert_attr(name="foo", value="baz")
@pytest.mark.filterwarnings("default")
def test_record_attribute(testdir): def test_record_attribute(testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
@ -1023,7 +1024,7 @@ def test_record_attribute(testdir):
tnode.assert_attr(bar="1") tnode.assert_attr(bar="1")
tnode.assert_attr(foo="<1") tnode.assert_attr(foo="<1")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
["test_record_attribute.py::test_record", "*record_xml_attribute*experimental*"] ["*test_record_attribute.py:6:*record_xml_attribute is an experimental feature"]
) )

View File

@ -16,7 +16,7 @@ from _pytest.mark import (
from _pytest.nodes import Node from _pytest.nodes import Node
ignore_markinfo = pytest.mark.filterwarnings( ignore_markinfo = pytest.mark.filterwarnings(
"ignore:MarkInfo objects:_pytest.deprecated.RemovedInPytest4Warning" "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning"
) )
@ -1039,10 +1039,19 @@ class TestKeywordSelection(object):
), ),
], ],
) )
@pytest.mark.filterwarnings("ignore") @pytest.mark.filterwarnings("default")
def test_parameterset_extractfrom(argval, expected): def test_parameterset_extractfrom(argval, expected):
extracted = ParameterSet.extract_from(argval) from _pytest.deprecated import MARK_PARAMETERSET_UNPACKING
warn_called = []
class DummyItem:
def warn(self, warning):
warn_called.append(warning)
extracted = ParameterSet.extract_from(argval, belonging_definition=DummyItem())
assert extracted == expected assert extracted == expected
assert warn_called == [MARK_PARAMETERSET_UNPACKING]
def test_legacy_transfer(): def test_legacy_transfer():

View File

@ -19,3 +19,14 @@ from _pytest import nodes
def test_ischildnode(baseid, nodeid, expected): def test_ischildnode(baseid, nodeid, expected):
result = nodes.ischildnode(baseid, nodeid) result = nodes.ischildnode(baseid, nodeid)
assert result is expected assert result is expected
def test_std_warn_not_pytestwarning(testdir):
items = testdir.getitems(
"""
def test():
pass
"""
)
with pytest.raises(ValueError, match=".*instance of PytestWarning.*"):
items[0].warn(UserWarning("some warning"))

View File

@ -7,7 +7,7 @@ from _pytest.recwarn import WarningsRecorder
def test_recwarn_functional(testdir): def test_recwarn_functional(testdir):
reprec = testdir.inline_runsource( testdir.makepyfile(
""" """
import warnings import warnings
def test_method(recwarn): def test_method(recwarn):
@ -16,8 +16,8 @@ def test_recwarn_functional(testdir):
assert isinstance(warn.message, UserWarning) assert isinstance(warn.message, UserWarning)
""" """
) )
res = reprec.countoutcomes() reprec = testdir.inline_run()
assert tuple(res) == (1, 0, 0), res reprec.assertoutcome(passed=1)
class TestWarningsRecorderChecker(object): class TestWarningsRecorderChecker(object):

View File

@ -13,6 +13,9 @@ from _pytest.resultlog import (
) )
pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated")
def test_generic_path(testdir): def test_generic_path(testdir):
from _pytest.main import Session from _pytest.main import Session

View File

@ -1047,20 +1047,21 @@ def test_terminal_summary(testdir):
) )
@pytest.mark.filterwarnings("default")
def test_terminal_summary_warnings_are_displayed(testdir): def test_terminal_summary_warnings_are_displayed(testdir):
"""Test that warnings emitted during pytest_terminal_summary are displayed. """Test that warnings emitted during pytest_terminal_summary are displayed.
(#1305). (#1305).
""" """
testdir.makeconftest( testdir.makeconftest(
""" """
import warnings
def pytest_terminal_summary(terminalreporter): def pytest_terminal_summary(terminalreporter):
config = terminalreporter.config warnings.warn(UserWarning('internal warning'))
config.warn('C1', 'internal warning')
""" """
) )
result = testdir.runpytest("-rw") result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
["<undetermined location>", "*internal warning", "*== 1 warnings in *"] ["*conftest.py:3:*internal warning", "*== 1 warnings in *"]
) )
assert "None" not in result.stdout.str() assert "None" not in result.stdout.str()

View File

@ -37,17 +37,15 @@ def pyfile_with_warnings(testdir, request):
) )
@pytest.mark.filterwarnings("always") @pytest.mark.filterwarnings("default")
def test_normal_flow(testdir, pyfile_with_warnings): def test_normal_flow(testdir, pyfile_with_warnings):
""" """
Check that the warnings section is displayed, containing test node ids followed by Check that the warnings section is displayed.
all warnings generated by that test node.
""" """
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*== %s ==*" % WARNINGS_SUMMARY_HEADER, "*== %s ==*" % WARNINGS_SUMMARY_HEADER,
"*test_normal_flow.py::test_func",
"*normal_flow_module.py:3: UserWarning: user warning", "*normal_flow_module.py:3: UserWarning: user warning",
'* warnings.warn(UserWarning("user warning"))', '* warnings.warn(UserWarning("user warning"))',
"*normal_flow_module.py:4: RuntimeWarning: runtime warning", "*normal_flow_module.py:4: RuntimeWarning: runtime warning",
@ -55,7 +53,6 @@ def test_normal_flow(testdir, pyfile_with_warnings):
"* 1 passed, 2 warnings*", "* 1 passed, 2 warnings*",
] ]
) )
assert result.stdout.str().count("test_normal_flow.py::test_func") == 1
@pytest.mark.filterwarnings("always") @pytest.mark.filterwarnings("always")
@ -302,3 +299,204 @@ def test_filterwarnings_mark_registration(testdir):
) )
result = testdir.runpytest("--strict") result = testdir.runpytest("--strict")
assert result.ret == 0 assert result.ret == 0
@pytest.mark.filterwarnings("always")
def test_warning_captured_hook(testdir):
testdir.makeconftest(
"""
from _pytest.warnings import _issue_config_warning
def pytest_configure(config):
_issue_config_warning(UserWarning("config warning"), config)
"""
)
testdir.makepyfile(
"""
import pytest, warnings
warnings.warn(UserWarning("collect warning"))
@pytest.fixture
def fix():
warnings.warn(UserWarning("setup warning"))
yield 1
warnings.warn(UserWarning("teardown warning"))
def test_func(fix):
warnings.warn(UserWarning("call warning"))
assert fix == 1
"""
)
collected = []
class WarningCollector:
def pytest_warning_captured(self, warning_message, when, item):
imge_name = item.name if item is not None else ""
collected.append((str(warning_message.message), when, imge_name))
result = testdir.runpytest(plugins=[WarningCollector()])
result.stdout.fnmatch_lines(["*1 passed*"])
expected = [
("config warning", "config", ""),
("collect warning", "collect", ""),
("setup warning", "runtest", "test_func"),
("call warning", "runtest", "test_func"),
("teardown warning", "runtest", "test_func"),
]
assert collected == expected
@pytest.mark.filterwarnings("always")
def test_collection_warnings(testdir):
"""
Check that we also capture warnings issued during test collection (#3251).
"""
testdir.makepyfile(
"""
import warnings
warnings.warn(UserWarning("collection warning"))
def test_foo():
pass
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(
[
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
"*collection_warnings.py:3: UserWarning: collection warning",
' warnings.warn(UserWarning("collection warning"))',
"* 1 passed, 1 warnings*",
]
)
@pytest.mark.filterwarnings("always")
def test_mark_regex_escape(testdir):
"""@pytest.mark.filterwarnings should not try to escape regex characters (#3936)"""
testdir.makepyfile(
r"""
import pytest, warnings
@pytest.mark.filterwarnings(r"ignore:some \(warning\)")
def test_foo():
warnings.warn(UserWarning("some (warning)"))
"""
)
result = testdir.runpytest()
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
@pytest.mark.filterwarnings("default")
@pytest.mark.parametrize("ignore_pytest_warnings", ["no", "ini", "cmdline"])
def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings):
"""Make sure we can ignore internal pytest warnings using a warnings filter."""
testdir.makepyfile(
"""
import pytest
import warnings
warnings.warn(pytest.PytestWarning("some internal warning"))
def test_bar():
pass
"""
)
if ignore_pytest_warnings == "ini":
testdir.makeini(
"""
[pytest]
filterwarnings = ignore::pytest.PytestWarning
"""
)
args = (
["-W", "ignore::pytest.PytestWarning"]
if ignore_pytest_warnings == "cmdline"
else []
)
result = testdir.runpytest(*args)
if ignore_pytest_warnings != "no":
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
else:
result.stdout.fnmatch_lines(
[
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
"*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning",
"* 1 passed, 1 warnings *",
]
)
class TestDeprecationWarningsByDefault:
"""
Note: all pytest runs are executed in a subprocess so we don't inherit warning filters
from pytest's own test suite
"""
def create_file(self, testdir, mark=""):
testdir.makepyfile(
"""
import pytest, warnings
warnings.warn(DeprecationWarning("collection"))
{mark}
def test_foo():
warnings.warn(PendingDeprecationWarning("test run"))
""".format(
mark=mark
)
)
def test_shown_by_default(self, testdir):
self.create_file(testdir)
result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines(
[
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
"*test_shown_by_default.py:3: DeprecationWarning: collection",
"*test_shown_by_default.py:7: PendingDeprecationWarning: test run",
"* 1 passed, 2 warnings*",
]
)
def test_hidden_by_ini(self, testdir):
self.create_file(testdir)
testdir.makeini(
"""
[pytest]
filterwarnings = once::UserWarning
"""
)
result = testdir.runpytest_subprocess()
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
def test_hidden_by_mark(self, testdir):
"""Should hide the deprecation warning from the function, but the warning during collection should
be displayed normally.
"""
self.create_file(
testdir, mark='@pytest.mark.filterwarnings("once::UserWarning")'
)
result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines(
[
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
"*test_hidden_by_mark.py:3: DeprecationWarning: collection",
"* 1 passed, 1 warnings*",
]
)
def test_hidden_by_cmdline(self, testdir):
self.create_file(testdir)
result = testdir.runpytest_subprocess("-W", "once::UserWarning")
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
def test_hidden_by_system(self, testdir, monkeypatch):
self.create_file(testdir)
monkeypatch.setenv(str("PYTHONWARNINGS"), str("once::UserWarning"))
result = testdir.runpytest_subprocess()
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()

View File

@ -218,6 +218,9 @@ norecursedirs = .tox ja .hg cx_freeze_source testing/example_scripts
xfail_strict=true xfail_strict=true
filterwarnings = filterwarnings =
error error
ignore:yield tests are deprecated, and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning
ignore:Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning
ignore:Module already imported so cannot be rewritten:pytest.PytestWarning
# produced by path.local # produced by path.local
ignore:bad escape.*:DeprecationWarning:re ignore:bad escape.*:DeprecationWarning:re
# produced by path.readlines # produced by path.readlines
@ -226,8 +229,8 @@ filterwarnings =
ignore:.*type argument to addoption.*:DeprecationWarning ignore:.*type argument to addoption.*:DeprecationWarning
# produced by python >=3.5 on execnet (pytest-xdist) # produced by python >=3.5 on execnet (pytest-xdist)
ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning
#pytests own futurewarnings # pytest's own futurewarnings
ignore::_pytest.experiments.PytestExerimentalApiWarning ignore::pytest.PytestExperimentalApiWarning
pytester_example_dir = testing/example_scripts pytester_example_dir = testing/example_scripts
[flake8] [flake8]
max-line-length = 120 max-line-length = 120