Merge branch 'pytest-dev:main' into fix-operation-on-closed-file-11572
This commit is contained in:
commit
c9c487977a
|
@ -26,7 +26,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v1.5.3
|
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v1.5.3
|
uses: hynek/build-and-inspect-python-package@v1.5.4
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [package]
|
needs: [package]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.10.1
|
rev: 23.11.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
|
@ -56,7 +56,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.6.1
|
rev: v1.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -293,6 +293,7 @@ Ondřej Súkup
|
||||||
Oscar Benjamin
|
Oscar Benjamin
|
||||||
Parth Patel
|
Parth Patel
|
||||||
Patrick Hayes
|
Patrick Hayes
|
||||||
|
Patrick Lannigan
|
||||||
Paul Müller
|
Paul Müller
|
||||||
Paul Reece
|
Paul Reece
|
||||||
Pauli Virtanen
|
Pauli Virtanen
|
||||||
|
@ -339,6 +340,7 @@ Saiprasad Kale
|
||||||
Samuel Colvin
|
Samuel Colvin
|
||||||
Samuel Dion-Girardeau
|
Samuel Dion-Girardeau
|
||||||
Samuel Searles-Bryant
|
Samuel Searles-Bryant
|
||||||
|
Samuel Therrien (Avasam)
|
||||||
Samuele Pedroni
|
Samuele Pedroni
|
||||||
Sanket Duthade
|
Sanket Duthade
|
||||||
Sankt Petersbug
|
Sankt Petersbug
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
:target: https://codecov.io/gh/pytest-dev/pytest
|
:target: https://codecov.io/gh/pytest-dev/pytest
|
||||||
:alt: Code coverage Status
|
:alt: Code coverage Status
|
||||||
|
|
||||||
.. image:: https://github.com/pytest-dev/pytest/workflows/test/badge.svg
|
.. image:: https://github.com/pytest-dev/pytest/actions/workflows/test.yml/badge.svg
|
||||||
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
|
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
|
||||||
|
|
||||||
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
|
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
Sanitized the handling of the ``default`` parameter when defining configuration options.
|
||||||
|
|
||||||
|
Previously if ``default`` was not supplied for :meth:`parser.addini <pytest.Parser.addini>` and the configuration option value was not defined in a test session, then calls to :func:`config.getini <pytest.Config.getini>` returned an *empty list* or an *empty string* depending on whether ``type`` was supplied or not respectively, which is clearly incorrect. Also, ``None`` was not honored even if ``default=None`` was used explicitly while defining the option.
|
||||||
|
|
||||||
|
Now the behavior of :meth:`parser.addini <pytest.Parser.addini>` is as follows:
|
||||||
|
|
||||||
|
* If ``default`` is NOT passed but ``type`` is provided, then a type-specific default will be returned. For example ``type=bool`` will return ``False``, ``type=str`` will return ``""``, etc.
|
||||||
|
* If ``default=None`` is passed and the option is not defined in a test session, then ``None`` will be returned, regardless of the ``type``.
|
||||||
|
* If neither ``default`` nor ``type`` are provided, assume ``type=str`` and return ``""`` as default (this is as per previous behavior).
|
||||||
|
|
||||||
|
The team decided to not introduce a deprecation period for this change, as doing so would be complicated both in terms of communicating this to the community as well as implementing it, and also because the team believes this change should not break existing plugins except in rare cases.
|
|
@ -0,0 +1,5 @@
|
||||||
|
Added the new :confval:`verbosity_assertions` configuration option for fine-grained control of failed assertions verbosity.
|
||||||
|
|
||||||
|
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
|
||||||
|
|
||||||
|
For plugin authors, :attr:`config.get_verbosity <pytest.Config.get_verbosity>` can be used to retrieve the verbosity level for a specific verbosity type.
|
|
@ -0,0 +1 @@
|
||||||
|
Improved the documentation and type signature for :func:`pytest.mark.xfail <pytest.mark.xfail>`'s ``condition`` param to use ``False`` as the default value.
|
|
@ -286,6 +286,20 @@ situations, for example you are shown even fixtures that start with ``_`` if you
|
||||||
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
|
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
|
||||||
however some plugins might make use of higher verbosity.
|
however some plugins might make use of higher verbosity.
|
||||||
|
|
||||||
|
.. _`pytest.fine_grained_verbosity`:
|
||||||
|
|
||||||
|
Fine-grained verbosity
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
|
||||||
|
This is done by setting a verbosity level in the configuration file for the specific aspect of the output.
|
||||||
|
|
||||||
|
:confval:`verbosity_assertions`: Controls how verbose the assertion output should be when pytest is executed. Running
|
||||||
|
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
|
||||||
|
the file is shown by a single character in the output.
|
||||||
|
|
||||||
|
(Note: currently this is the only option available, but more might be added in the future).
|
||||||
|
|
||||||
.. _`pytest.detailed_failed_tests_usage`:
|
.. _`pytest.detailed_failed_tests_usage`:
|
||||||
|
|
||||||
Producing a detailed summary report
|
Producing a detailed summary report
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -239,12 +239,11 @@ pytest.mark.xfail
|
||||||
|
|
||||||
Marks a test function as *expected to fail*.
|
Marks a test function as *expected to fail*.
|
||||||
|
|
||||||
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=xfail_strict)
|
.. py:function:: pytest.mark.xfail(condition=False, *, reason=None, raises=None, run=True, strict=xfail_strict)
|
||||||
|
|
||||||
:type condition: bool or str
|
:keyword Union[bool, str] condition:
|
||||||
:param condition:
|
|
||||||
Condition for marking the test function as xfail (``True/False`` or a
|
Condition for marking the test function as xfail (``True/False`` or a
|
||||||
:ref:`condition string <string conditions>`). If a bool, you also have
|
:ref:`condition string <string conditions>`). If a ``bool``, you also have
|
||||||
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
||||||
:keyword str reason:
|
:keyword str reason:
|
||||||
Reason why the test function is marked as xfail.
|
Reason why the test function is marked as xfail.
|
||||||
|
@ -1823,6 +1822,19 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
clean_db
|
clean_db
|
||||||
|
|
||||||
|
|
||||||
|
.. confval:: verbosity_assertions
|
||||||
|
|
||||||
|
Set a verbosity level specifically for assertion related output, overriding the application wide level.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
|
||||||
|
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
|
||||||
|
"auto" can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
|
|
||||||
.. confval:: xfail_strict
|
.. confval:: xfail_strict
|
||||||
|
|
||||||
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
||||||
|
|
|
@ -0,0 +1,626 @@
|
||||||
|
# This module was imported from the cpython standard library
|
||||||
|
# (https://github.com/python/cpython/) at commit
|
||||||
|
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Original Author: Fred L. Drake, Jr.
|
||||||
|
# fdrake@acm.org
|
||||||
|
#
|
||||||
|
# This is a simple little module I wrote to make life easier. I didn't
|
||||||
|
# see anything quite like it in the library, though I may have overlooked
|
||||||
|
# something. I wrote this when I was trying to read some heavily nested
|
||||||
|
# tuples with fairly non-descriptive content. This is modeled very much
|
||||||
|
# after Lisp/Scheme - style pretty-printing of lists. If you find it
|
||||||
|
# useful, thank small children who sleep at night.
|
||||||
|
import collections as _collections
|
||||||
|
import dataclasses as _dataclasses
|
||||||
|
import re
|
||||||
|
import sys as _sys
|
||||||
|
import types as _types
|
||||||
|
from io import StringIO as _StringIO
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import IO
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class _safe_key:
|
||||||
|
"""Helper function for key functions when sorting unorderable objects.
|
||||||
|
|
||||||
|
The wrapped-object will fallback to a Py2.x style comparison for
|
||||||
|
unorderable types (sorting first comparing the type name and then by
|
||||||
|
the obj ids). Does not work recursively, so dict.items() must have
|
||||||
|
_safe_key applied to both the key and the value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ["obj"]
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
try:
|
||||||
|
return self.obj < other.obj
|
||||||
|
except TypeError:
|
||||||
|
return (str(type(self.obj)), id(self.obj)) < (
|
||||||
|
str(type(other.obj)),
|
||||||
|
id(other.obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_tuple(t):
|
||||||
|
"""Helper function for comparing 2-tuples"""
|
||||||
|
return _safe_key(t[0]), _safe_key(t[1])
|
||||||
|
|
||||||
|
|
||||||
|
class PrettyPrinter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
indent=1,
|
||||||
|
width=80,
|
||||||
|
depth=None,
|
||||||
|
stream=None,
|
||||||
|
*,
|
||||||
|
compact=False,
|
||||||
|
sort_dicts=True,
|
||||||
|
underscore_numbers=False,
|
||||||
|
):
|
||||||
|
"""Handle pretty printing operations onto a stream using a set of
|
||||||
|
configured parameters.
|
||||||
|
|
||||||
|
indent
|
||||||
|
Number of spaces to indent for each level of nesting.
|
||||||
|
|
||||||
|
width
|
||||||
|
Attempted maximum number of columns in the output.
|
||||||
|
|
||||||
|
depth
|
||||||
|
The maximum depth to print out nested structures.
|
||||||
|
|
||||||
|
stream
|
||||||
|
The desired output stream. If omitted (or false), the standard
|
||||||
|
output stream available at construction will be used.
|
||||||
|
|
||||||
|
compact
|
||||||
|
If true, several items will be combined in one line.
|
||||||
|
|
||||||
|
sort_dicts
|
||||||
|
If true, dict keys are sorted.
|
||||||
|
|
||||||
|
"""
|
||||||
|
indent = int(indent)
|
||||||
|
width = int(width)
|
||||||
|
if indent < 0:
|
||||||
|
raise ValueError("indent must be >= 0")
|
||||||
|
if depth is not None and depth <= 0:
|
||||||
|
raise ValueError("depth must be > 0")
|
||||||
|
if not width:
|
||||||
|
raise ValueError("width must be != 0")
|
||||||
|
self._depth = depth
|
||||||
|
self._indent_per_level = indent
|
||||||
|
self._width = width
|
||||||
|
if stream is not None:
|
||||||
|
self._stream = stream
|
||||||
|
else:
|
||||||
|
self._stream = _sys.stdout
|
||||||
|
self._compact = bool(compact)
|
||||||
|
self._sort_dicts = sort_dicts
|
||||||
|
self._underscore_numbers = underscore_numbers
|
||||||
|
|
||||||
|
def pformat(self, object: Any) -> str:
|
||||||
|
sio = _StringIO()
|
||||||
|
self._format(object, sio, 0, 0, {}, 0)
|
||||||
|
return sio.getvalue()
|
||||||
|
|
||||||
|
def _format(self, object, stream, indent, allowance, context, level):
|
||||||
|
objid = id(object)
|
||||||
|
if objid in context:
|
||||||
|
stream.write(_recursion(object))
|
||||||
|
self._recursive = True
|
||||||
|
self._readable = False
|
||||||
|
return
|
||||||
|
|
||||||
|
p = self._dispatch.get(type(object).__repr__, None)
|
||||||
|
if p is not None:
|
||||||
|
context[objid] = 1
|
||||||
|
p(self, object, stream, indent, allowance, context, level + 1)
|
||||||
|
del context[objid]
|
||||||
|
elif (
|
||||||
|
_dataclasses.is_dataclass(object)
|
||||||
|
and not isinstance(object, type)
|
||||||
|
and object.__dataclass_params__.repr
|
||||||
|
and
|
||||||
|
# Check dataclass has generated repr method.
|
||||||
|
hasattr(object.__repr__, "__wrapped__")
|
||||||
|
and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
|
||||||
|
):
|
||||||
|
context[objid] = 1
|
||||||
|
self._pprint_dataclass(
|
||||||
|
object, stream, indent, allowance, context, level + 1
|
||||||
|
)
|
||||||
|
del context[objid]
|
||||||
|
else:
|
||||||
|
stream.write(self._repr(object, context, level))
|
||||||
|
|
||||||
|
def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
|
||||||
|
cls_name = object.__class__.__name__
|
||||||
|
indent += len(cls_name) + 1
|
||||||
|
items = [
|
||||||
|
(f.name, getattr(object, f.name))
|
||||||
|
for f in _dataclasses.fields(object)
|
||||||
|
if f.repr
|
||||||
|
]
|
||||||
|
stream.write(cls_name + "(")
|
||||||
|
self._format_namespace_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch: Dict[
|
||||||
|
Callable[..., str],
|
||||||
|
Callable[["PrettyPrinter", Any, IO[str], int, int, Dict[int, int], int], str],
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
def _pprint_dict(self, object, stream, indent, allowance, context, level):
|
||||||
|
write = stream.write
|
||||||
|
write("{")
|
||||||
|
if self._indent_per_level > 1:
|
||||||
|
write((self._indent_per_level - 1) * " ")
|
||||||
|
length = len(object)
|
||||||
|
if length:
|
||||||
|
if self._sort_dicts:
|
||||||
|
items = sorted(object.items(), key=_safe_tuple)
|
||||||
|
else:
|
||||||
|
items = object.items()
|
||||||
|
self._format_dict_items(
|
||||||
|
items, stream, indent, allowance + 1, context, level
|
||||||
|
)
|
||||||
|
write("}")
|
||||||
|
|
||||||
|
_dispatch[dict.__repr__] = _pprint_dict
|
||||||
|
|
||||||
|
def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level):
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
cls = object.__class__
|
||||||
|
stream.write(cls.__name__ + "(")
|
||||||
|
self._format(
|
||||||
|
list(object.items()),
|
||||||
|
stream,
|
||||||
|
indent + len(cls.__name__) + 1,
|
||||||
|
allowance + 1,
|
||||||
|
context,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
|
||||||
|
|
||||||
|
def _pprint_list(self, object, stream, indent, allowance, context, level):
|
||||||
|
stream.write("[")
|
||||||
|
self._format_items(object, stream, indent, allowance + 1, context, level)
|
||||||
|
stream.write("]")
|
||||||
|
|
||||||
|
_dispatch[list.__repr__] = _pprint_list
|
||||||
|
|
||||||
|
def _pprint_tuple(self, object, stream, indent, allowance, context, level):
|
||||||
|
stream.write("(")
|
||||||
|
endchar = ",)" if len(object) == 1 else ")"
|
||||||
|
self._format_items(
|
||||||
|
object, stream, indent, allowance + len(endchar), context, level
|
||||||
|
)
|
||||||
|
stream.write(endchar)
|
||||||
|
|
||||||
|
_dispatch[tuple.__repr__] = _pprint_tuple
|
||||||
|
|
||||||
|
def _pprint_set(self, object, stream, indent, allowance, context, level):
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
typ = object.__class__
|
||||||
|
if typ is set:
|
||||||
|
stream.write("{")
|
||||||
|
endchar = "}"
|
||||||
|
else:
|
||||||
|
stream.write(typ.__name__ + "({")
|
||||||
|
endchar = "})"
|
||||||
|
indent += len(typ.__name__) + 1
|
||||||
|
object = sorted(object, key=_safe_key)
|
||||||
|
self._format_items(
|
||||||
|
object, stream, indent, allowance + len(endchar), context, level
|
||||||
|
)
|
||||||
|
stream.write(endchar)
|
||||||
|
|
||||||
|
_dispatch[set.__repr__] = _pprint_set
|
||||||
|
_dispatch[frozenset.__repr__] = _pprint_set
|
||||||
|
|
||||||
|
def _pprint_str(self, object, stream, indent, allowance, context, level):
|
||||||
|
write = stream.write
|
||||||
|
if not len(object):
|
||||||
|
write(repr(object))
|
||||||
|
return
|
||||||
|
chunks = []
|
||||||
|
lines = object.splitlines(True)
|
||||||
|
if level == 1:
|
||||||
|
indent += 1
|
||||||
|
allowance += 1
|
||||||
|
max_width1 = max_width = self._width - indent
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
rep = repr(line)
|
||||||
|
if i == len(lines) - 1:
|
||||||
|
max_width1 -= allowance
|
||||||
|
if len(rep) <= max_width1:
|
||||||
|
chunks.append(rep)
|
||||||
|
else:
|
||||||
|
# A list of alternating (non-space, space) strings
|
||||||
|
parts = re.findall(r"\S*\s*", line)
|
||||||
|
assert parts
|
||||||
|
assert not parts[-1]
|
||||||
|
parts.pop() # drop empty last part
|
||||||
|
max_width2 = max_width
|
||||||
|
current = ""
|
||||||
|
for j, part in enumerate(parts):
|
||||||
|
candidate = current + part
|
||||||
|
if j == len(parts) - 1 and i == len(lines) - 1:
|
||||||
|
max_width2 -= allowance
|
||||||
|
if len(repr(candidate)) > max_width2:
|
||||||
|
if current:
|
||||||
|
chunks.append(repr(current))
|
||||||
|
current = part
|
||||||
|
else:
|
||||||
|
current = candidate
|
||||||
|
if current:
|
||||||
|
chunks.append(repr(current))
|
||||||
|
if len(chunks) == 1:
|
||||||
|
write(rep)
|
||||||
|
return
|
||||||
|
if level == 1:
|
||||||
|
write("(")
|
||||||
|
for i, rep in enumerate(chunks):
|
||||||
|
if i > 0:
|
||||||
|
write("\n" + " " * indent)
|
||||||
|
write(rep)
|
||||||
|
if level == 1:
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[str.__repr__] = _pprint_str
|
||||||
|
|
||||||
|
def _pprint_bytes(self, object, stream, indent, allowance, context, level):
|
||||||
|
write = stream.write
|
||||||
|
if len(object) <= 4:
|
||||||
|
write(repr(object))
|
||||||
|
return
|
||||||
|
parens = level == 1
|
||||||
|
if parens:
|
||||||
|
indent += 1
|
||||||
|
allowance += 1
|
||||||
|
write("(")
|
||||||
|
delim = ""
|
||||||
|
for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
|
||||||
|
write(delim)
|
||||||
|
write(rep)
|
||||||
|
if not delim:
|
||||||
|
delim = "\n" + " " * indent
|
||||||
|
if parens:
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[bytes.__repr__] = _pprint_bytes
|
||||||
|
|
||||||
|
def _pprint_bytearray(self, object, stream, indent, allowance, context, level):
|
||||||
|
write = stream.write
|
||||||
|
write("bytearray(")
|
||||||
|
self._pprint_bytes(
|
||||||
|
bytes(object), stream, indent + 10, allowance + 1, context, level + 1
|
||||||
|
)
|
||||||
|
write(")")
|
||||||
|
|
||||||
|
_dispatch[bytearray.__repr__] = _pprint_bytearray
|
||||||
|
|
||||||
|
def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
|
||||||
|
stream.write("mappingproxy(")
|
||||||
|
self._format(object.copy(), stream, indent + 13, allowance + 1, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
|
||||||
|
|
||||||
|
def _pprint_simplenamespace(
|
||||||
|
self, object, stream, indent, allowance, context, level
|
||||||
|
):
|
||||||
|
if type(object) is _types.SimpleNamespace:
|
||||||
|
# The SimpleNamespace repr is "namespace" instead of the class
|
||||||
|
# name, so we do the same here. For subclasses; use the class name.
|
||||||
|
cls_name = "namespace"
|
||||||
|
else:
|
||||||
|
cls_name = object.__class__.__name__
|
||||||
|
indent += len(cls_name) + 1
|
||||||
|
items = object.__dict__.items()
|
||||||
|
stream.write(cls_name + "(")
|
||||||
|
self._format_namespace_items(items, stream, indent, allowance, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
|
||||||
|
|
||||||
|
def _format_dict_items(self, items, stream, indent, allowance, context, level):
|
||||||
|
write = stream.write
|
||||||
|
indent += self._indent_per_level
|
||||||
|
delimnl = ",\n" + " " * indent
|
||||||
|
last_index = len(items) - 1
|
||||||
|
for i, (key, ent) in enumerate(items):
|
||||||
|
last = i == last_index
|
||||||
|
rep = self._repr(key, context, level)
|
||||||
|
write(rep)
|
||||||
|
write(": ")
|
||||||
|
self._format(
|
||||||
|
ent,
|
||||||
|
stream,
|
||||||
|
indent + len(rep) + 2,
|
||||||
|
allowance if last else 1,
|
||||||
|
context,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
if not last:
|
||||||
|
write(delimnl)
|
||||||
|
|
||||||
|
def _format_namespace_items(self, items, stream, indent, allowance, context, level):
|
||||||
|
write = stream.write
|
||||||
|
delimnl = ",\n" + " " * indent
|
||||||
|
last_index = len(items) - 1
|
||||||
|
for i, (key, ent) in enumerate(items):
|
||||||
|
last = i == last_index
|
||||||
|
write(key)
|
||||||
|
write("=")
|
||||||
|
if id(ent) in context:
|
||||||
|
# Special-case representation of recursion to match standard
|
||||||
|
# recursive dataclass repr.
|
||||||
|
write("...")
|
||||||
|
else:
|
||||||
|
self._format(
|
||||||
|
ent,
|
||||||
|
stream,
|
||||||
|
indent + len(key) + 1,
|
||||||
|
allowance if last else 1,
|
||||||
|
context,
|
||||||
|
level,
|
||||||
|
)
|
||||||
|
if not last:
|
||||||
|
write(delimnl)
|
||||||
|
|
||||||
|
def _format_items(self, items, stream, indent, allowance, context, level):
|
||||||
|
write = stream.write
|
||||||
|
indent += self._indent_per_level
|
||||||
|
if self._indent_per_level > 1:
|
||||||
|
write((self._indent_per_level - 1) * " ")
|
||||||
|
delimnl = ",\n" + " " * indent
|
||||||
|
delim = ""
|
||||||
|
width = max_width = self._width - indent + 1
|
||||||
|
it = iter(items)
|
||||||
|
try:
|
||||||
|
next_ent = next(it)
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
last = False
|
||||||
|
while not last:
|
||||||
|
ent = next_ent
|
||||||
|
try:
|
||||||
|
next_ent = next(it)
|
||||||
|
except StopIteration:
|
||||||
|
last = True
|
||||||
|
max_width -= allowance
|
||||||
|
width -= allowance
|
||||||
|
if self._compact:
|
||||||
|
rep = self._repr(ent, context, level)
|
||||||
|
w = len(rep) + 2
|
||||||
|
if width < w:
|
||||||
|
width = max_width
|
||||||
|
if delim:
|
||||||
|
delim = delimnl
|
||||||
|
if width >= w:
|
||||||
|
width -= w
|
||||||
|
write(delim)
|
||||||
|
delim = ", "
|
||||||
|
write(rep)
|
||||||
|
continue
|
||||||
|
write(delim)
|
||||||
|
delim = delimnl
|
||||||
|
self._format(ent, stream, indent, allowance if last else 1, context, level)
|
||||||
|
|
||||||
|
def _repr(self, object, context, level):
|
||||||
|
repr, readable, recursive = self.format(
|
||||||
|
object, context.copy(), self._depth, level
|
||||||
|
)
|
||||||
|
if not readable:
|
||||||
|
self._readable = False
|
||||||
|
if recursive:
|
||||||
|
self._recursive = True
|
||||||
|
return repr
|
||||||
|
|
||||||
|
def format(self, object, context, maxlevels, level):
|
||||||
|
"""Format object for a specific context, returning a string
|
||||||
|
and flags indicating whether the representation is 'readable'
|
||||||
|
and whether the object represents a recursive construct.
|
||||||
|
"""
|
||||||
|
return self._safe_repr(object, context, maxlevels, level)
|
||||||
|
|
||||||
|
def _pprint_default_dict(self, object, stream, indent, allowance, context, level):
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
rdf = self._repr(object.default_factory, context, level)
|
||||||
|
cls = object.__class__
|
||||||
|
indent += len(cls.__name__) + 1
|
||||||
|
stream.write(f"{cls.__name__}({rdf},\n{' ' * indent}")
|
||||||
|
self._pprint_dict(object, stream, indent, allowance + 1, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
|
||||||
|
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
|
||||||
|
|
||||||
|
def _pprint_counter(self, object, stream, indent, allowance, context, level):
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
cls = object.__class__
|
||||||
|
stream.write(cls.__name__ + "({")
|
||||||
|
if self._indent_per_level > 1:
|
||||||
|
stream.write((self._indent_per_level - 1) * " ")
|
||||||
|
items = object.most_common()
|
||||||
|
self._format_dict_items(
|
||||||
|
items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level
|
||||||
|
)
|
||||||
|
stream.write("})")
|
||||||
|
|
||||||
|
_dispatch[_collections.Counter.__repr__] = _pprint_counter
|
||||||
|
|
||||||
|
def _pprint_chain_map(self, object, stream, indent, allowance, context, level):
|
||||||
|
if not len(object.maps):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
cls = object.__class__
|
||||||
|
stream.write(cls.__name__ + "(")
|
||||||
|
indent += len(cls.__name__) + 1
|
||||||
|
for i, m in enumerate(object.maps):
|
||||||
|
if i == len(object.maps) - 1:
|
||||||
|
self._format(m, stream, indent, allowance + 1, context, level)
|
||||||
|
stream.write(")")
|
||||||
|
else:
|
||||||
|
self._format(m, stream, indent, 1, context, level)
|
||||||
|
stream.write(",\n" + " " * indent)
|
||||||
|
|
||||||
|
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
|
||||||
|
|
||||||
|
def _pprint_deque(self, object, stream, indent, allowance, context, level):
|
||||||
|
if not len(object):
|
||||||
|
stream.write(repr(object))
|
||||||
|
return
|
||||||
|
cls = object.__class__
|
||||||
|
stream.write(cls.__name__ + "(")
|
||||||
|
indent += len(cls.__name__) + 1
|
||||||
|
stream.write("[")
|
||||||
|
if object.maxlen is None:
|
||||||
|
self._format_items(object, stream, indent, allowance + 2, context, level)
|
||||||
|
stream.write("])")
|
||||||
|
else:
|
||||||
|
self._format_items(object, stream, indent, 2, context, level)
|
||||||
|
rml = self._repr(object.maxlen, context, level)
|
||||||
|
stream.write(f"],\n{' ' * indent}maxlen={rml})")
|
||||||
|
|
||||||
|
_dispatch[_collections.deque.__repr__] = _pprint_deque
|
||||||
|
|
||||||
|
def _pprint_user_dict(self, object, stream, indent, allowance, context, level):
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
|
||||||
|
|
||||||
|
def _pprint_user_list(self, object, stream, indent, allowance, context, level):
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserList.__repr__] = _pprint_user_list
|
||||||
|
|
||||||
|
def _pprint_user_string(self, object, stream, indent, allowance, context, level):
|
||||||
|
self._format(object.data, stream, indent, allowance, context, level - 1)
|
||||||
|
|
||||||
|
_dispatch[_collections.UserString.__repr__] = _pprint_user_string
|
||||||
|
|
||||||
|
def _safe_repr(self, object, context, maxlevels, level):
|
||||||
|
# Return triple (repr_string, isreadable, isrecursive).
|
||||||
|
typ = type(object)
|
||||||
|
if typ in _builtin_scalars:
|
||||||
|
return repr(object), True, False
|
||||||
|
|
||||||
|
r = getattr(typ, "__repr__", None)
|
||||||
|
|
||||||
|
if issubclass(typ, int) and r is int.__repr__:
|
||||||
|
if self._underscore_numbers:
|
||||||
|
return f"{object:_d}", True, False
|
||||||
|
else:
|
||||||
|
return repr(object), True, False
|
||||||
|
|
||||||
|
if issubclass(typ, dict) and r is dict.__repr__:
|
||||||
|
if not object:
|
||||||
|
return "{}", True, False
|
||||||
|
objid = id(object)
|
||||||
|
if maxlevels and level >= maxlevels:
|
||||||
|
return "{...}", False, objid in context
|
||||||
|
if objid in context:
|
||||||
|
return _recursion(object), False, True
|
||||||
|
context[objid] = 1
|
||||||
|
readable = True
|
||||||
|
recursive = False
|
||||||
|
components: List[str] = []
|
||||||
|
append = components.append
|
||||||
|
level += 1
|
||||||
|
if self._sort_dicts:
|
||||||
|
items = sorted(object.items(), key=_safe_tuple)
|
||||||
|
else:
|
||||||
|
items = object.items()
|
||||||
|
for k, v in items:
|
||||||
|
krepr, kreadable, krecur = self.format(k, context, maxlevels, level)
|
||||||
|
vrepr, vreadable, vrecur = self.format(v, context, maxlevels, level)
|
||||||
|
append(f"{krepr}: {vrepr}")
|
||||||
|
readable = readable and kreadable and vreadable
|
||||||
|
if krecur or vrecur:
|
||||||
|
recursive = True
|
||||||
|
del context[objid]
|
||||||
|
return "{%s}" % ", ".join(components), readable, recursive
|
||||||
|
|
||||||
|
if (issubclass(typ, list) and r is list.__repr__) or (
|
||||||
|
issubclass(typ, tuple) and r is tuple.__repr__
|
||||||
|
):
|
||||||
|
if issubclass(typ, list):
|
||||||
|
if not object:
|
||||||
|
return "[]", True, False
|
||||||
|
format = "[%s]"
|
||||||
|
elif len(object) == 1:
|
||||||
|
format = "(%s,)"
|
||||||
|
else:
|
||||||
|
if not object:
|
||||||
|
return "()", True, False
|
||||||
|
format = "(%s)"
|
||||||
|
objid = id(object)
|
||||||
|
if maxlevels and level >= maxlevels:
|
||||||
|
return format % "...", False, objid in context
|
||||||
|
if objid in context:
|
||||||
|
return _recursion(object), False, True
|
||||||
|
context[objid] = 1
|
||||||
|
readable = True
|
||||||
|
recursive = False
|
||||||
|
components = []
|
||||||
|
append = components.append
|
||||||
|
level += 1
|
||||||
|
for o in object:
|
||||||
|
orepr, oreadable, orecur = self.format(o, context, maxlevels, level)
|
||||||
|
append(orepr)
|
||||||
|
if not oreadable:
|
||||||
|
readable = False
|
||||||
|
if orecur:
|
||||||
|
recursive = True
|
||||||
|
del context[objid]
|
||||||
|
return format % ", ".join(components), readable, recursive
|
||||||
|
|
||||||
|
rep = repr(object)
|
||||||
|
return rep, (rep and not rep.startswith("<")), False
|
||||||
|
|
||||||
|
|
||||||
|
_builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)})
|
||||||
|
|
||||||
|
|
||||||
|
def _recursion(object):
|
||||||
|
return f"<Recursion on {type(object).__name__} with id={id(object)}>"
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_bytes_repr(object, width, allowance):
|
||||||
|
current = b""
|
||||||
|
last = len(object) // 4 * 4
|
||||||
|
for i in range(0, len(object), 4):
|
||||||
|
part = object[i : i + 4]
|
||||||
|
candidate = current + part
|
||||||
|
if i == last:
|
||||||
|
width -= allowance
|
||||||
|
if len(repr(candidate)) > width:
|
||||||
|
if current:
|
||||||
|
yield repr(current)
|
||||||
|
current = part
|
||||||
|
else:
|
||||||
|
current = candidate
|
||||||
|
if current:
|
||||||
|
yield repr(current)
|
|
@ -1,8 +1,5 @@
|
||||||
import pprint
|
import pprint
|
||||||
import reprlib
|
import reprlib
|
||||||
from typing import Any
|
|
||||||
from typing import Dict
|
|
||||||
from typing import IO
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,49 +129,3 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
|
||||||
return repr(obj)
|
return repr(obj)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return _format_repr_exception(exc, obj)
|
return _format_repr_exception(exc, obj)
|
||||||
|
|
||||||
|
|
||||||
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
|
||||||
"""PrettyPrinter that always dispatches (regardless of width)."""
|
|
||||||
|
|
||||||
def _format(
|
|
||||||
self,
|
|
||||||
object: object,
|
|
||||||
stream: IO[str],
|
|
||||||
indent: int,
|
|
||||||
allowance: int,
|
|
||||||
context: Dict[int, Any],
|
|
||||||
level: int,
|
|
||||||
) -> None:
|
|
||||||
# Type ignored because _dispatch is private.
|
|
||||||
p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
objid = id(object)
|
|
||||||
if objid in context or p is None:
|
|
||||||
# Type ignored because _format is private.
|
|
||||||
super()._format( # type: ignore[misc]
|
|
||||||
object,
|
|
||||||
stream,
|
|
||||||
indent,
|
|
||||||
allowance,
|
|
||||||
context,
|
|
||||||
level,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
context[objid] = 1
|
|
||||||
p(self, object, stream, indent, allowance, context, level + 1)
|
|
||||||
del context[objid]
|
|
||||||
|
|
||||||
|
|
||||||
def _pformat_dispatch(
|
|
||||||
object: object,
|
|
||||||
indent: int = 1,
|
|
||||||
width: int = 80,
|
|
||||||
depth: Optional[int] = None,
|
|
||||||
*,
|
|
||||||
compact: bool = False,
|
|
||||||
) -> str:
|
|
||||||
return AlwaysDispatchingPrettyPrinter(
|
|
||||||
indent=indent, width=width, depth=depth, compact=compact
|
|
||||||
).pformat(object)
|
|
||||||
|
|
|
@ -42,6 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
help="Enables the pytest_assertion_pass hook. "
|
help="Enables the pytest_assertion_pass hook. "
|
||||||
"Make sure to delete any previously generated pyc cache files.",
|
"Make sure to delete any previously generated pyc cache files.",
|
||||||
)
|
)
|
||||||
|
Config._add_verbosity_ini(
|
||||||
|
parser,
|
||||||
|
Config.VERBOSITY_ASSERTIONS,
|
||||||
|
help=(
|
||||||
|
"Specify a verbosity level for assertions, overriding the main level. "
|
||||||
|
"Higher levels will provide more detailed explanation when an assertion fails."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_assert_rewrite(*names: str) -> None:
|
def register_assert_rewrite(*names: str) -> None:
|
||||||
|
|
|
@ -426,7 +426,10 @@ def _saferepr(obj: object) -> str:
|
||||||
|
|
||||||
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
|
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
|
||||||
"""Get `maxsize` configuration for saferepr based on the given config object."""
|
"""Get `maxsize` configuration for saferepr based on the given config object."""
|
||||||
verbosity = config.getoption("verbose") if config is not None else 0
|
if config is None:
|
||||||
|
verbosity = 0
|
||||||
|
else:
|
||||||
|
verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
if verbosity >= 2:
|
if verbosity >= 2:
|
||||||
return None
|
return None
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
"""Utilities for truncating assertion output.
|
"""Utilities for truncating assertion output.
|
||||||
|
|
||||||
Current default behaviour is to truncate assertion explanations at
|
Current default behaviour is to truncate assertion explanations at
|
||||||
~8 terminal lines, unless running in "-vv" mode or running on CI.
|
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
|
||||||
"""
|
"""
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ def truncate_if_required(
|
||||||
|
|
||||||
def _should_truncate_item(item: Item) -> bool:
|
def _should_truncate_item(item: Item) -> bool:
|
||||||
"""Whether or not this test item is eligible for truncation."""
|
"""Whether or not this test item is eligible for truncation."""
|
||||||
verbose = item.config.option.verbose
|
verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
return verbose < 2 and not util.running_on_ci()
|
return verbose < 2 and not util.running_on_ci()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ from unicodedata import normalize
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest._io.saferepr import _pformat_dispatch
|
from _pytest._io.pprint import PrettyPrinter
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._io.saferepr import saferepr_unlimited
|
from _pytest._io.saferepr import saferepr_unlimited
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
@ -168,7 +168,7 @@ def assertrepr_compare(
|
||||||
config, op: str, left: Any, right: Any, use_ascii: bool = False
|
config, op: str, left: Any, right: Any, use_ascii: bool = False
|
||||||
) -> Optional[List[str]]:
|
) -> Optional[List[str]]:
|
||||||
"""Return specialised explanations for some operators/operands."""
|
"""Return specialised explanations for some operators/operands."""
|
||||||
verbose = config.getoption("verbose")
|
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
|
||||||
|
|
||||||
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
|
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
|
||||||
# See issue #3246.
|
# See issue #3246.
|
||||||
|
@ -348,8 +348,9 @@ def _compare_eq_iterable(
|
||||||
lines_left = len(left_formatting)
|
lines_left = len(left_formatting)
|
||||||
lines_right = len(right_formatting)
|
lines_right = len(right_formatting)
|
||||||
if lines_left != lines_right:
|
if lines_left != lines_right:
|
||||||
left_formatting = _pformat_dispatch(left).splitlines()
|
printer = PrettyPrinter()
|
||||||
right_formatting = _pformat_dispatch(right).splitlines()
|
left_formatting = printer.pformat(left).splitlines()
|
||||||
|
right_formatting = printer.pformat(right).splitlines()
|
||||||
|
|
||||||
if lines_left > 1 or lines_right > 1:
|
if lines_left > 1 or lines_right > 1:
|
||||||
_surrounding_parens_on_own_lines(left_formatting)
|
_surrounding_parens_on_own_lines(left_formatting)
|
||||||
|
|
|
@ -22,6 +22,7 @@ from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import Final
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
@ -69,7 +70,7 @@ from _pytest.warning_types import warn_explicit_for
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest._code.code import _TracebackStyle
|
from _pytest._code.code import _TracebackStyle
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
from .argparsing import Argument
|
from .argparsing import Argument, Parser
|
||||||
|
|
||||||
|
|
||||||
_PluggyPlugin = object
|
_PluggyPlugin = object
|
||||||
|
@ -1495,6 +1496,27 @@ class Config:
|
||||||
def getini(self, name: str):
|
def getini(self, name: str):
|
||||||
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
||||||
|
|
||||||
|
If a configuration value is not defined in an
|
||||||
|
:ref:`ini file <configfiles>`, then the ``default`` value provided while
|
||||||
|
registering the configuration through
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
||||||
|
Please note that you can even provide ``None`` as a valid
|
||||||
|
default value.
|
||||||
|
|
||||||
|
If ``default`` is not provided while registering using
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>`, then a default value
|
||||||
|
based on the ``type`` parameter passed to
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>` will be returned.
|
||||||
|
The default values based on ``type`` are:
|
||||||
|
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
|
||||||
|
``bool`` : ``False``
|
||||||
|
``string`` : empty string ``""``
|
||||||
|
|
||||||
|
If neither the ``default`` nor the ``type`` parameter is passed
|
||||||
|
while registering the configuration through
|
||||||
|
:func:`parser.addini <pytest.Parser.addini>`, then the configuration
|
||||||
|
is treated as a string and a default empty string '' is returned.
|
||||||
|
|
||||||
If the specified name hasn't been registered through a prior
|
If the specified name hasn't been registered through a prior
|
||||||
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
|
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
|
||||||
plugin), a ValueError is raised.
|
plugin), a ValueError is raised.
|
||||||
|
@ -1521,11 +1543,7 @@ class Config:
|
||||||
try:
|
try:
|
||||||
value = self.inicfg[name]
|
value = self.inicfg[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if default is not None:
|
return default
|
||||||
return default
|
|
||||||
if type is None:
|
|
||||||
return ""
|
|
||||||
return []
|
|
||||||
else:
|
else:
|
||||||
value = override_value
|
value = override_value
|
||||||
# Coerce the values based on types.
|
# Coerce the values based on types.
|
||||||
|
@ -1633,6 +1651,78 @@ class Config:
|
||||||
"""Deprecated, use getoption(skip=True) instead."""
|
"""Deprecated, use getoption(skip=True) instead."""
|
||||||
return self.getoption(name, skip=True)
|
return self.getoption(name, skip=True)
|
||||||
|
|
||||||
|
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
|
||||||
|
VERBOSITY_ASSERTIONS: Final = "assertions"
|
||||||
|
_VERBOSITY_INI_DEFAULT: Final = "auto"
|
||||||
|
|
||||||
|
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||||
|
r"""Retrieve the verbosity level for a fine-grained verbosity type.
|
||||||
|
|
||||||
|
:param verbosity_type: Verbosity type to get level for. If a level is
|
||||||
|
configured for the given type, that value will be returned. If the
|
||||||
|
given type is not a known verbosity type, the global verbosity
|
||||||
|
level will be returned. If the given type is None (default), the
|
||||||
|
global verbosity level will be returned.
|
||||||
|
|
||||||
|
To configure a level for a fine-grained verbosity type, the
|
||||||
|
configuration file should have a setting for the configuration name
|
||||||
|
and a numeric value for the verbosity level. A special value of "auto"
|
||||||
|
can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
# content of pytest.ini
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest -v
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(config.get_verbosity()) # 1
|
||||||
|
print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
|
||||||
|
"""
|
||||||
|
global_level = self.option.verbose
|
||||||
|
assert isinstance(global_level, int)
|
||||||
|
if verbosity_type is None:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
ini_name = Config._verbosity_ini_name(verbosity_type)
|
||||||
|
if ini_name not in self._parser._inidict:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
level = self.getini(ini_name)
|
||||||
|
if level == Config._VERBOSITY_INI_DEFAULT:
|
||||||
|
return global_level
|
||||||
|
|
||||||
|
return int(level)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _verbosity_ini_name(verbosity_type: str) -> str:
|
||||||
|
return f"verbosity_{verbosity_type}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None:
|
||||||
|
"""Add a output verbosity configuration option for the given output type.
|
||||||
|
|
||||||
|
:param parser: Parser for command line arguments and ini-file values.
|
||||||
|
:param verbosity_type: Fine-grained verbosity category.
|
||||||
|
:param help: Description of the output this type controls.
|
||||||
|
|
||||||
|
The value should be retrieved via a call to
|
||||||
|
:py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
|
||||||
|
"""
|
||||||
|
parser.addini(
|
||||||
|
Config._verbosity_ini_name(verbosity_type),
|
||||||
|
help=help,
|
||||||
|
type="string",
|
||||||
|
default=Config._VERBOSITY_INI_DEFAULT,
|
||||||
|
)
|
||||||
|
|
||||||
def _warn_about_missing_assertion(self, mode: str) -> None:
|
def _warn_about_missing_assertion(self, mode: str) -> None:
|
||||||
if not _assertion_supported():
|
if not _assertion_supported():
|
||||||
if mode == "plain":
|
if mode == "plain":
|
||||||
|
|
|
@ -27,6 +27,14 @@ from _pytest.deprecated import check_ispytest
|
||||||
FILE_OR_DIR = "file_or_dir"
|
FILE_OR_DIR = "file_or_dir"
|
||||||
|
|
||||||
|
|
||||||
|
class NotSet:
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "<notset>"
|
||||||
|
|
||||||
|
|
||||||
|
NOT_SET = NotSet()
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
class Parser:
|
class Parser:
|
||||||
"""Parser for command line arguments and ini-file values.
|
"""Parser for command line arguments and ini-file values.
|
||||||
|
@ -176,7 +184,7 @@ class Parser:
|
||||||
type: Optional[
|
type: Optional[
|
||||||
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
||||||
] = None,
|
] = None,
|
||||||
default: Any = None,
|
default: Any = NOT_SET,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register an ini-file option.
|
"""Register an ini-file option.
|
||||||
|
|
||||||
|
@ -203,10 +211,30 @@ class Parser:
|
||||||
:py:func:`config.getini(name) <pytest.Config.getini>`.
|
:py:func:`config.getini(name) <pytest.Config.getini>`.
|
||||||
"""
|
"""
|
||||||
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
|
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
|
||||||
|
if default is NOT_SET:
|
||||||
|
default = get_ini_default_for_type(type)
|
||||||
|
|
||||||
self._inidict[name] = (help, type, default)
|
self._inidict[name] = (help, type, default)
|
||||||
self._ininames.append(name)
|
self._ininames.append(name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ini_default_for_type(
|
||||||
|
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Used by addini to get the default value for a given ini-option type, when
|
||||||
|
default is not supplied.
|
||||||
|
"""
|
||||||
|
if type is None:
|
||||||
|
return ""
|
||||||
|
elif type in ("paths", "pathlist", "args", "linelist"):
|
||||||
|
return []
|
||||||
|
elif type == "bool":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class ArgumentError(Exception):
|
class ArgumentError(Exception):
|
||||||
"""Raised if an Argument instance is created with invalid or
|
"""Raised if an Argument instance is created with invalid or
|
||||||
inconsistent arguments."""
|
inconsistent arguments."""
|
||||||
|
|
|
@ -457,7 +457,7 @@ if TYPE_CHECKING:
|
||||||
@overload
|
@overload
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
condition: Union[str, bool] = ...,
|
condition: Union[str, bool] = False,
|
||||||
*conditions: Union[str, bool],
|
*conditions: Union[str, bool],
|
||||||
reason: str = ...,
|
reason: str = ...,
|
||||||
run: bool = ...,
|
run: bool = ...,
|
||||||
|
|
|
@ -8,3 +8,5 @@ import _pytest._py.path as path
|
||||||
|
|
||||||
sys.modules["py.error"] = error
|
sys.modules["py.error"] = error
|
||||||
sys.modules["py.path"] = path
|
sys.modules["py.path"] = path
|
||||||
|
|
||||||
|
__all__ = ["error", "path"]
|
||||||
|
|
|
@ -868,6 +868,9 @@ class TestLocalPath(CommonFSTests):
|
||||||
py_path.strpath, str_path
|
py_path.strpath, str_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pytest.mark.xfail(
|
||||||
|
reason="#11603", raises=(error.EEXIST, error.ENOENT), strict=False
|
||||||
|
)
|
||||||
def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
|
def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
|
||||||
# https://github.com/pytest-dev/py/issues/30
|
# https://github.com/pytest-dev/py/issues/30
|
||||||
with multiprocessing.Pool() as pool:
|
with multiprocessing.Pool() as pool:
|
||||||
|
|
|
@ -0,0 +1,330 @@
|
||||||
|
import textwrap
|
||||||
|
from collections import ChainMap
|
||||||
|
from collections import Counter
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections import deque
|
||||||
|
from collections import OrderedDict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from types import MappingProxyType
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest._io.pprint import PrettyPrinter
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EmptyDataclass:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DataclassWithOneItem:
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DataclassWithTwoItems:
|
||||||
|
foo: str
|
||||||
|
bar: str
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("data", "expected"),
|
||||||
|
(
|
||||||
|
pytest.param(
|
||||||
|
EmptyDataclass(),
|
||||||
|
"EmptyDataclass()",
|
||||||
|
id="dataclass-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
DataclassWithOneItem(foo="bar"),
|
||||||
|
"""
|
||||||
|
DataclassWithOneItem(foo='bar')
|
||||||
|
""",
|
||||||
|
id="dataclass-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
DataclassWithTwoItems(foo="foo", bar="bar"),
|
||||||
|
"""
|
||||||
|
DataclassWithTwoItems(foo='foo',
|
||||||
|
bar='bar')
|
||||||
|
""",
|
||||||
|
id="dataclass-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{},
|
||||||
|
"{}",
|
||||||
|
id="dict-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"one": 1},
|
||||||
|
"""
|
||||||
|
{'one': 1}
|
||||||
|
""",
|
||||||
|
id="dict-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"one": 1, "two": 2},
|
||||||
|
"""
|
||||||
|
{'one': 1,
|
||||||
|
'two': 2}
|
||||||
|
""",
|
||||||
|
id="dict-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(OrderedDict(), "OrderedDict()", id="ordereddict-empty"),
|
||||||
|
pytest.param(
|
||||||
|
OrderedDict({"one": 1}),
|
||||||
|
"""
|
||||||
|
OrderedDict([('one',
|
||||||
|
1)])
|
||||||
|
""",
|
||||||
|
id="ordereddict-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
OrderedDict({"one": 1, "two": 2}),
|
||||||
|
"""
|
||||||
|
OrderedDict([('one',
|
||||||
|
1),
|
||||||
|
('two',
|
||||||
|
2)])
|
||||||
|
""",
|
||||||
|
id="ordereddict-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[],
|
||||||
|
"[]",
|
||||||
|
id="list-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[1],
|
||||||
|
"""
|
||||||
|
[1]
|
||||||
|
""",
|
||||||
|
id="list-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[1, 2],
|
||||||
|
"""
|
||||||
|
[1,
|
||||||
|
2]
|
||||||
|
""",
|
||||||
|
id="list-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
tuple(),
|
||||||
|
"()",
|
||||||
|
id="tuple-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
(1,),
|
||||||
|
"""
|
||||||
|
(1,)
|
||||||
|
""",
|
||||||
|
id="tuple-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
(1, 2),
|
||||||
|
"""
|
||||||
|
(1,
|
||||||
|
2)
|
||||||
|
""",
|
||||||
|
id="tuple-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
set(),
|
||||||
|
"set()",
|
||||||
|
id="set-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{1},
|
||||||
|
"""
|
||||||
|
{1}
|
||||||
|
""",
|
||||||
|
id="set-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{1, 2},
|
||||||
|
"""
|
||||||
|
{1,
|
||||||
|
2}
|
||||||
|
""",
|
||||||
|
id="set-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
MappingProxyType({}),
|
||||||
|
"mappingproxy({})",
|
||||||
|
id="mappingproxy-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
MappingProxyType({"one": 1}),
|
||||||
|
"""
|
||||||
|
mappingproxy({'one': 1})
|
||||||
|
""",
|
||||||
|
id="mappingproxy-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
MappingProxyType({"one": 1, "two": 2}),
|
||||||
|
"""
|
||||||
|
mappingproxy({'one': 1,
|
||||||
|
'two': 2})
|
||||||
|
""",
|
||||||
|
id="mappingproxy-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
SimpleNamespace(),
|
||||||
|
"namespace()",
|
||||||
|
id="simplenamespace-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
SimpleNamespace(one=1),
|
||||||
|
"""
|
||||||
|
namespace(one=1)
|
||||||
|
""",
|
||||||
|
id="simplenamespace-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
SimpleNamespace(one=1, two=2),
|
||||||
|
"""
|
||||||
|
namespace(one=1,
|
||||||
|
two=2)
|
||||||
|
""",
|
||||||
|
id="simplenamespace-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
defaultdict(str), "defaultdict(<class 'str'>, {})", id="defaultdict-empty"
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
defaultdict(str, {"one": "1"}),
|
||||||
|
"""
|
||||||
|
defaultdict(<class 'str'>,
|
||||||
|
{'one': '1'})
|
||||||
|
""",
|
||||||
|
id="defaultdict-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
defaultdict(str, {"one": "1", "two": "2"}),
|
||||||
|
"""
|
||||||
|
defaultdict(<class 'str'>,
|
||||||
|
{'one': '1',
|
||||||
|
'two': '2'})
|
||||||
|
""",
|
||||||
|
id="defaultdict-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Counter(),
|
||||||
|
"Counter()",
|
||||||
|
id="counter-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Counter("1"),
|
||||||
|
"""
|
||||||
|
Counter({'1': 1})
|
||||||
|
""",
|
||||||
|
id="counter-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Counter("121"),
|
||||||
|
"""
|
||||||
|
Counter({'1': 2,
|
||||||
|
'2': 1})
|
||||||
|
""",
|
||||||
|
id="counter-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(ChainMap(), "ChainMap({})", id="chainmap-empty"),
|
||||||
|
pytest.param(
|
||||||
|
ChainMap({"one": 1, "two": 2}),
|
||||||
|
"""
|
||||||
|
ChainMap({'one': 1,
|
||||||
|
'two': 2})
|
||||||
|
""",
|
||||||
|
id="chainmap-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
ChainMap({"one": 1}, {"two": 2}),
|
||||||
|
"""
|
||||||
|
ChainMap({'one': 1},
|
||||||
|
{'two': 2})
|
||||||
|
""",
|
||||||
|
id="chainmap-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque(),
|
||||||
|
"deque([])",
|
||||||
|
id="deque-empty",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque([1]),
|
||||||
|
"""
|
||||||
|
deque([1])
|
||||||
|
""",
|
||||||
|
id="deque-one-item",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque([1, 2]),
|
||||||
|
"""
|
||||||
|
deque([1,
|
||||||
|
2])
|
||||||
|
""",
|
||||||
|
id="deque-two-items",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
deque([1, 2], maxlen=3),
|
||||||
|
"""
|
||||||
|
deque([1,
|
||||||
|
2],
|
||||||
|
maxlen=3)
|
||||||
|
""",
|
||||||
|
id="deque-maxlen",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"chainmap": ChainMap({"one": 1}, {"two": 2}),
|
||||||
|
"counter": Counter("122"),
|
||||||
|
"dataclass": DataclassWithTwoItems(foo="foo", bar="bar"),
|
||||||
|
"defaultdict": defaultdict(str, {"one": "1", "two": "2"}),
|
||||||
|
"deque": deque([1, 2], maxlen=3),
|
||||||
|
"dict": {"one": 1, "two": 2},
|
||||||
|
"list": [1, 2],
|
||||||
|
"mappingproxy": MappingProxyType({"one": 1, "two": 2}),
|
||||||
|
"ordereddict": OrderedDict({"one": 1, "two": 2}),
|
||||||
|
"set": {1, 2},
|
||||||
|
"simplenamespace": SimpleNamespace(one=1, two=2),
|
||||||
|
"tuple": (1, 2),
|
||||||
|
},
|
||||||
|
"""
|
||||||
|
{'chainmap': ChainMap({'one': 1},
|
||||||
|
{'two': 2}),
|
||||||
|
'counter': Counter({'2': 2,
|
||||||
|
'1': 1}),
|
||||||
|
'dataclass': DataclassWithTwoItems(foo='foo',
|
||||||
|
bar='bar'),
|
||||||
|
'defaultdict': defaultdict(<class 'str'>,
|
||||||
|
{'one': '1',
|
||||||
|
'two': '2'}),
|
||||||
|
'deque': deque([1,
|
||||||
|
2],
|
||||||
|
maxlen=3),
|
||||||
|
'dict': {'one': 1,
|
||||||
|
'two': 2},
|
||||||
|
'list': [1,
|
||||||
|
2],
|
||||||
|
'mappingproxy': mappingproxy({'one': 1,
|
||||||
|
'two': 2}),
|
||||||
|
'ordereddict': OrderedDict([('one',
|
||||||
|
1),
|
||||||
|
('two',
|
||||||
|
2)]),
|
||||||
|
'set': {1,
|
||||||
|
2},
|
||||||
|
'simplenamespace': namespace(one=1,
|
||||||
|
two=2),
|
||||||
|
'tuple': (1,
|
||||||
|
2)}
|
||||||
|
""",
|
||||||
|
id="deep-example",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_consistent_pretty_printer(data: Any, expected: str) -> None:
|
||||||
|
assert PrettyPrinter().pformat(data) == textwrap.dedent(expected).strip()
|
|
@ -1,5 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest._io.saferepr import _pformat_dispatch
|
|
||||||
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._io.saferepr import saferepr_unlimited
|
from _pytest._io.saferepr import saferepr_unlimited
|
||||||
|
@ -159,12 +158,6 @@ def test_unicode():
|
||||||
assert saferepr(val) == reprval
|
assert saferepr(val) == reprval
|
||||||
|
|
||||||
|
|
||||||
def test_pformat_dispatch():
|
|
||||||
assert _pformat_dispatch("a") == "'a'"
|
|
||||||
assert _pformat_dispatch("a" * 10, width=5) == "'aaaaaaaaaa'"
|
|
||||||
assert _pformat_dispatch("foo bar", width=5) == "('foo '\n 'bar')"
|
|
||||||
|
|
||||||
|
|
||||||
def test_broken_getattribute():
|
def test_broken_getattribute():
|
||||||
"""saferepr() can create proper representations of classes with
|
"""saferepr() can create proper representations of classes with
|
||||||
broken __getattribute__ (#7145)
|
broken __getattribute__ (#7145)
|
||||||
|
|
|
@ -3,13 +3,13 @@ django==4.2.7
|
||||||
pytest-asyncio==0.21.1
|
pytest-asyncio==0.21.1
|
||||||
pytest-bdd==7.0.0
|
pytest-bdd==7.0.0
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
pytest-django==4.5.2
|
pytest-django==4.7.0
|
||||||
pytest-flakes==4.0.5
|
pytest-flakes==4.0.5
|
||||||
pytest-html==4.0.2
|
pytest-html==4.1.1
|
||||||
pytest-mock==3.12.0
|
pytest-mock==3.12.0
|
||||||
pytest-rerunfailures==12.0
|
pytest-rerunfailures==12.0
|
||||||
pytest-sugar==0.9.7
|
pytest-sugar==0.9.7
|
||||||
pytest-trio==0.7.0
|
pytest-trio==0.7.0
|
||||||
pytest-twisted==1.14.0
|
pytest-twisted==1.14.0
|
||||||
twisted==23.8.0
|
twisted==23.10.0
|
||||||
pytest-xvfb==3.0.0
|
pytest-xvfb==3.0.0
|
||||||
|
|
|
@ -13,27 +13,68 @@ import pytest
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest.assertion import truncate
|
from _pytest.assertion import truncate
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
|
from _pytest.config import Config as _Config
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
|
||||||
|
|
||||||
def mock_config(verbose=0):
|
def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
|
||||||
class TerminalWriter:
|
class TerminalWriter:
|
||||||
def _highlight(self, source, lexer):
|
def _highlight(self, source, lexer):
|
||||||
return source
|
return source
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def getoption(self, name):
|
|
||||||
if name == "verbose":
|
|
||||||
return verbose
|
|
||||||
raise KeyError("Not mocked out: %s" % name)
|
|
||||||
|
|
||||||
def get_terminal_writer(self):
|
def get_terminal_writer(self):
|
||||||
return TerminalWriter()
|
return TerminalWriter()
|
||||||
|
|
||||||
|
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||||
|
if verbosity_type is None:
|
||||||
|
return verbose
|
||||||
|
if verbosity_type == _Config.VERBOSITY_ASSERTIONS:
|
||||||
|
if assertion_override is not None:
|
||||||
|
return assertion_override
|
||||||
|
return verbose
|
||||||
|
|
||||||
|
raise KeyError(f"Not mocked out: {verbosity_type}")
|
||||||
|
|
||||||
return Config()
|
return Config()
|
||||||
|
|
||||||
|
|
||||||
|
class TestMockConfig:
|
||||||
|
SOME_VERBOSITY_LEVEL = 3
|
||||||
|
SOME_OTHER_VERBOSITY_LEVEL = 10
|
||||||
|
|
||||||
|
def test_verbose_exposes_value(self):
|
||||||
|
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
|
||||||
|
|
||||||
|
assert config.get_verbosity() == TestMockConfig.SOME_VERBOSITY_LEVEL
|
||||||
|
|
||||||
|
def test_get_assertion_override_not_set_verbose_value(self):
|
||||||
|
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
|
||||||
|
== TestMockConfig.SOME_VERBOSITY_LEVEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_assertion_override_set_custom_value(self):
|
||||||
|
config = mock_config(
|
||||||
|
verbose=TestMockConfig.SOME_VERBOSITY_LEVEL,
|
||||||
|
assertion_override=TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
|
||||||
|
== TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_unsupported_type_error(self):
|
||||||
|
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
config.get_verbosity("--- NOT A VERBOSITY LEVEL ---")
|
||||||
|
|
||||||
|
|
||||||
class TestImportHookInstallation:
|
class TestImportHookInstallation:
|
||||||
@pytest.mark.parametrize("initial_conftest", [True, False])
|
@pytest.mark.parametrize("initial_conftest", [True, False])
|
||||||
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
|
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
|
||||||
|
@ -1836,3 +1877,54 @@ def test_comparisons_handle_colors(
|
||||||
)
|
)
|
||||||
|
|
||||||
result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False)
|
result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fine_grained_assertion_verbosity(pytester: Pytester):
|
||||||
|
long_text = "Lorem ipsum dolor sit amet " * 10
|
||||||
|
p = pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
def test_ok():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_words_fail():
|
||||||
|
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
|
||||||
|
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
|
||||||
|
assert fruits1 == fruits2
|
||||||
|
|
||||||
|
|
||||||
|
def test_numbers_fail():
|
||||||
|
number_to_text1 = {{str(x): x for x in range(5)}}
|
||||||
|
number_to_text2 = {{str(x * 10): x * 10 for x in range(5)}}
|
||||||
|
assert number_to_text1 == number_to_text2
|
||||||
|
|
||||||
|
|
||||||
|
def test_long_text_fail():
|
||||||
|
long_text = "{long_text}"
|
||||||
|
assert "hello world" in long_text
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
verbosity_assertions = 2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(p)
|
||||||
|
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
f"{p.name} .FFF [100%]",
|
||||||
|
"E At index 2 diff: 'grapes' != 'orange'",
|
||||||
|
"E Full diff:",
|
||||||
|
"E - ['banana', 'apple', 'orange', 'melon', 'kiwi']",
|
||||||
|
"E ? ^ ^^",
|
||||||
|
"E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']",
|
||||||
|
"E ? ^ ^ +",
|
||||||
|
"E Full diff:",
|
||||||
|
"E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}",
|
||||||
|
"E ? - - - - - - - -",
|
||||||
|
"E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}",
|
||||||
|
f"E AssertionError: assert 'hello world' in '{long_text}'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -2056,13 +2056,15 @@ class TestReprSizeVerbosity:
|
||||||
)
|
)
|
||||||
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
|
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
|
||||||
class FakeConfig:
|
class FakeConfig:
|
||||||
def getoption(self, name: str) -> int:
|
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||||
assert name == "verbose"
|
|
||||||
return verbose
|
return verbose
|
||||||
|
|
||||||
config = FakeConfig()
|
config = FakeConfig()
|
||||||
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size
|
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size
|
||||||
|
|
||||||
|
def test_get_maxsize_for_saferepr_no_config(self) -> None:
|
||||||
|
assert _get_maxsize_for_saferepr(None) == DEFAULT_REPR_MAX_SIZE
|
||||||
|
|
||||||
def create_test_file(self, pytester: Pytester, size: int) -> None:
|
def create_test_file(self, pytester: Pytester, size: int) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
f"""
|
f"""
|
||||||
|
|
|
@ -5,6 +5,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
@ -21,6 +22,8 @@ from _pytest.config import Config
|
||||||
from _pytest.config import ConftestImportFailure
|
from _pytest.config import ConftestImportFailure
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import parse_warning_filter
|
from _pytest.config import parse_warning_filter
|
||||||
|
from _pytest.config.argparsing import get_ini_default_for_type
|
||||||
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.config.findpaths import determine_setup
|
from _pytest.config.findpaths import determine_setup
|
||||||
from _pytest.config.findpaths import get_common_ancestor
|
from _pytest.config.findpaths import get_common_ancestor
|
||||||
|
@ -857,6 +860,68 @@ class TestConfigAPI:
|
||||||
assert len(values) == 2
|
assert len(values) == 2
|
||||||
assert values == ["456", "123"]
|
assert values == ["456", "123"]
|
||||||
|
|
||||||
|
def test_addini_default_values(self, pytester: Pytester) -> None:
|
||||||
|
"""Tests the default values for configuration based on
|
||||||
|
config type
|
||||||
|
"""
|
||||||
|
|
||||||
|
pytester.makeconftest(
|
||||||
|
"""
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addini("linelist1", "", type="linelist")
|
||||||
|
parser.addini("paths1", "", type="paths")
|
||||||
|
parser.addini("pathlist1", "", type="pathlist")
|
||||||
|
parser.addini("args1", "", type="args")
|
||||||
|
parser.addini("bool1", "", type="bool")
|
||||||
|
parser.addini("string1", "", type="string")
|
||||||
|
parser.addini("none_1", "", type="linelist", default=None)
|
||||||
|
parser.addini("none_2", "", default=None)
|
||||||
|
parser.addini("no_type", "")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
config = pytester.parseconfig()
|
||||||
|
# default for linelist, paths, pathlist and args is []
|
||||||
|
value = config.getini("linelist1")
|
||||||
|
assert value == []
|
||||||
|
value = config.getini("paths1")
|
||||||
|
assert value == []
|
||||||
|
value = config.getini("pathlist1")
|
||||||
|
assert value == []
|
||||||
|
value = config.getini("args1")
|
||||||
|
assert value == []
|
||||||
|
# default for bool is False
|
||||||
|
value = config.getini("bool1")
|
||||||
|
assert value is False
|
||||||
|
# default for string is ""
|
||||||
|
value = config.getini("string1")
|
||||||
|
assert value == ""
|
||||||
|
# should return None if None is explicity set as default value
|
||||||
|
# irrespective of the type argument
|
||||||
|
value = config.getini("none_1")
|
||||||
|
assert value is None
|
||||||
|
value = config.getini("none_2")
|
||||||
|
assert value is None
|
||||||
|
# in case no type is provided and no default set
|
||||||
|
# treat it as string and default value will be ""
|
||||||
|
value = config.getini("no_type")
|
||||||
|
assert value == ""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"type, expected",
|
||||||
|
[
|
||||||
|
pytest.param(None, "", id="None"),
|
||||||
|
pytest.param("string", "", id="string"),
|
||||||
|
pytest.param("paths", [], id="paths"),
|
||||||
|
pytest.param("pathlist", [], id="pathlist"),
|
||||||
|
pytest.param("args", [], id="args"),
|
||||||
|
pytest.param("linelist", [], id="linelist"),
|
||||||
|
pytest.param("bool", False, id="bool"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_ini_default_for_type(self, type: Any, expected: Any) -> None:
|
||||||
|
assert get_ini_default_for_type(type) == expected
|
||||||
|
|
||||||
def test_confcutdir_check_isdir(self, pytester: Pytester) -> None:
|
def test_confcutdir_check_isdir(self, pytester: Pytester) -> None:
|
||||||
"""Give an error if --confcutdir is not a valid directory (#2078)"""
|
"""Give an error if --confcutdir is not a valid directory (#2078)"""
|
||||||
exp_match = r"^--confcutdir must be a directory, given: "
|
exp_match = r"^--confcutdir must be a directory, given: "
|
||||||
|
@ -2181,3 +2246,76 @@ class TestDebugOptions:
|
||||||
"*Default: pytestdebug.log.",
|
"*Default: pytestdebug.log.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerbosity:
|
||||||
|
SOME_OUTPUT_TYPE = Config.VERBOSITY_ASSERTIONS
|
||||||
|
SOME_OUTPUT_VERBOSITY_LEVEL = 5
|
||||||
|
|
||||||
|
class VerbosityIni:
|
||||||
|
def pytest_addoption(self, parser: Parser) -> None:
|
||||||
|
Config._add_verbosity_ini(
|
||||||
|
parser, TestVerbosity.SOME_OUTPUT_TYPE, help="some help text"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_level_matches_verbose_when_not_specified(
|
||||||
|
self, pytester: Pytester, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
tmp_path.joinpath("pytest.ini").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
[pytest]
|
||||||
|
addopts = --verbose
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
pytester.plugins = [TestVerbosity.VerbosityIni()]
|
||||||
|
|
||||||
|
config = pytester.parseconfig(tmp_path)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE)
|
||||||
|
== config.option.verbose
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_level_matches_verbose_when_not_known_type(
|
||||||
|
self, pytester: Pytester, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
tmp_path.joinpath("pytest.ini").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
[pytest]
|
||||||
|
addopts = --verbose
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
pytester.plugins = [TestVerbosity.VerbosityIni()]
|
||||||
|
|
||||||
|
config = pytester.parseconfig(tmp_path)
|
||||||
|
|
||||||
|
assert config.get_verbosity("some fake verbosity type") == config.option.verbose
|
||||||
|
|
||||||
|
def test_level_matches_specified_override(
|
||||||
|
self, pytester: Pytester, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
setting_name = f"verbosity_{TestVerbosity.SOME_OUTPUT_TYPE}"
|
||||||
|
tmp_path.joinpath("pytest.ini").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
[pytest]
|
||||||
|
addopts = --verbose
|
||||||
|
{setting_name} = {TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
pytester.plugins = [TestVerbosity.VerbosityIni()]
|
||||||
|
|
||||||
|
config = pytester.parseconfig(tmp_path)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE)
|
||||||
|
== TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue