Merge branch 'pytest-dev:main' into fix-operation-on-closed-file-11572

This commit is contained in:
Simon Blanchard 2023-11-21 18:43:19 +01:00 committed by GitHub
commit c9c487977a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1609 additions and 224 deletions

View File

@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- 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:
if: github.repository == 'pytest-dev/pytest'

View File

@ -35,7 +35,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- 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:
needs: [package]

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 23.10.1
rev: 23.11.0
hooks:
- id: black
args: [--safe, --quiet]
@ -56,7 +56,7 @@ repos:
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.6.1
rev: v1.7.0
hooks:
- id: mypy
files: ^(src/|testing/)

View File

@ -293,6 +293,7 @@ Ondřej Súkup
Oscar Benjamin
Parth Patel
Patrick Hayes
Patrick Lannigan
Paul Müller
Paul Reece
Pauli Virtanen
@ -339,6 +340,7 @@ Saiprasad Kale
Samuel Colvin
Samuel Dion-Girardeau
Samuel Searles-Bryant
Samuel Therrien (Avasam)
Samuele Pedroni
Sanket Duthade
Sankt Petersbug

View File

@ -20,7 +20,7 @@
:target: https://codecov.io/gh/pytest-dev/pytest
: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
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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,
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`:
Producing a detailed summary report

File diff suppressed because it is too large Load Diff

View File

@ -239,12 +239,11 @@ pytest.mark.xfail
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
:param condition:
:keyword Union[bool, str] condition:
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>`).
:keyword str reason:
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
.. 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
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the

626
src/_pytest/_io/pprint.py Normal file
View File

@ -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)

View File

@ -1,8 +1,5 @@
import pprint
import reprlib
from typing import Any
from typing import Dict
from typing import IO
from typing import Optional
@ -132,49 +129,3 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
return repr(obj)
except Exception as exc:
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)

View File

@ -42,6 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
help="Enables the pytest_assertion_pass hook. "
"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:

View File

@ -426,7 +426,10 @@ def _saferepr(obj: object) -> str:
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
"""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:
return None
if verbosity >= 1:

View File

@ -1,12 +1,13 @@
"""Utilities for truncating assertion output.
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 Optional
from _pytest.assertion import util
from _pytest.config import Config
from _pytest.nodes import Item
@ -26,7 +27,7 @@ def truncate_if_required(
def _should_truncate_item(item: Item) -> bool:
"""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()

View File

@ -16,7 +16,7 @@ from unicodedata import normalize
import _pytest._code
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_unlimited
from _pytest.config import Config
@ -168,7 +168,7 @@ def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]:
"""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.
# See issue #3246.
@ -348,8 +348,9 @@ def _compare_eq_iterable(
lines_left = len(left_formatting)
lines_right = len(right_formatting)
if lines_left != lines_right:
left_formatting = _pformat_dispatch(left).splitlines()
right_formatting = _pformat_dispatch(right).splitlines()
printer = PrettyPrinter()
left_formatting = printer.pformat(left).splitlines()
right_formatting = printer.pformat(right).splitlines()
if lines_left > 1 or lines_right > 1:
_surrounding_parens_on_own_lines(left_formatting)

View File

@ -22,6 +22,7 @@ from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import Final
from typing import final
from typing import Generator
from typing import IO
@ -69,7 +70,7 @@ from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument
from .argparsing import Argument, Parser
_PluggyPlugin = object
@ -1495,6 +1496,27 @@ class Config:
def getini(self, name: str):
"""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
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
plugin), a ValueError is raised.
@ -1521,11 +1543,7 @@ class Config:
try:
value = self.inicfg[name]
except KeyError:
if default is not None:
return default
if type is None:
return ""
return []
return default
else:
value = override_value
# Coerce the values based on types.
@ -1633,6 +1651,78 @@ class Config:
"""Deprecated, use getoption(skip=True) instead."""
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:
if not _assertion_supported():
if mode == "plain":

View File

@ -27,6 +27,14 @@ from _pytest.deprecated import check_ispytest
FILE_OR_DIR = "file_or_dir"
class NotSet:
def __repr__(self) -> str:
return "<notset>"
NOT_SET = NotSet()
@final
class Parser:
"""Parser for command line arguments and ini-file values.
@ -176,7 +184,7 @@ class Parser:
type: Optional[
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
] = None,
default: Any = None,
default: Any = NOT_SET,
) -> None:
"""Register an ini-file option.
@ -203,10 +211,30 @@ class Parser:
:py:func:`config.getini(name) <pytest.Config.getini>`.
"""
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._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):
"""Raised if an Argument instance is created with invalid or
inconsistent arguments."""

View File

@ -457,7 +457,7 @@ if TYPE_CHECKING:
@overload
def __call__(
self,
condition: Union[str, bool] = ...,
condition: Union[str, bool] = False,
*conditions: Union[str, bool],
reason: str = ...,
run: bool = ...,

View File

@ -8,3 +8,5 @@ import _pytest._py.path as path
sys.modules["py.error"] = error
sys.modules["py.path"] = path
__all__ = ["error", "path"]

View File

@ -868,6 +868,9 @@ class TestLocalPath(CommonFSTests):
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):
# https://github.com/pytest-dev/py/issues/30
with multiprocessing.Pool() as pool:

330
testing/io/test_pprint.py Normal file
View File

@ -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()

View File

@ -1,5 +1,4 @@
import pytest
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
@ -159,12 +158,6 @@ def test_unicode():
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():
"""saferepr() can create proper representations of classes with
broken __getattribute__ (#7145)

View File

@ -3,13 +3,13 @@ django==4.2.7
pytest-asyncio==0.21.1
pytest-bdd==7.0.0
pytest-cov==4.1.0
pytest-django==4.5.2
pytest-django==4.7.0
pytest-flakes==4.0.5
pytest-html==4.0.2
pytest-html==4.1.1
pytest-mock==3.12.0
pytest-rerunfailures==12.0
pytest-sugar==0.9.7
pytest-trio==0.7.0
pytest-twisted==1.14.0
twisted==23.8.0
twisted==23.10.0
pytest-xvfb==3.0.0

View File

@ -13,27 +13,68 @@ import pytest
from _pytest import outcomes
from _pytest.assertion import truncate
from _pytest.assertion import util
from _pytest.config import Config as _Config
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
def mock_config(verbose=0):
def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
class TerminalWriter:
def _highlight(self, source, lexer):
return source
class Config:
def getoption(self, name):
if name == "verbose":
return verbose
raise KeyError("Not mocked out: %s" % name)
def get_terminal_writer(self):
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()
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:
@pytest.mark.parametrize("initial_conftest", [True, False])
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
@ -1836,3 +1877,54 @@ def test_comparisons_handle_colors(
)
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}'",
]
)

View File

@ -2056,13 +2056,15 @@ class TestReprSizeVerbosity:
)
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
class FakeConfig:
def getoption(self, name: str) -> int:
assert name == "verbose"
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
return verbose
config = FakeConfig()
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:
pytester.makepyfile(
f"""

View File

@ -5,6 +5,7 @@ import re
import sys
import textwrap
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Sequence
@ -21,6 +22,8 @@ from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import ExitCode
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.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor
@ -857,6 +860,68 @@ class TestConfigAPI:
assert len(values) == 2
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:
"""Give an error if --confcutdir is not a valid directory (#2078)"""
exp_match = r"^--confcutdir must be a directory, given: "
@ -2181,3 +2246,76 @@ class TestDebugOptions:
"*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
)