#!/usr/bin/env python
#
# params.py
"""
`pytest.mark.parametrize <https://docs.pytest.org/en/stable/parametrize.html>`_ decorators.
"""
#
# Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# "param" based on pytest
# Copyright (c) 2004-2020 Holger Krekel and others
# MIT Licensed
#
# stdlib
import itertools
import random
from typing import Callable, Collection, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union, cast, overload
# 3rd party
import pytest
from _pytest.mark import Mark, MarkDecorator, ParameterSet # nodep
from domdf_python_tools.iterative import extend_with_none
# this package
from coincidence.selectors import _make_version, only_version
from coincidence.utils import generate_falsy_values, generate_truthy_values, whitespace_perms_list
__all__ = ("count", "whitespace_perms", "testing_boolean_values", "param", "parametrized_versions")
_T = TypeVar("_T")
MarkDecorator.__module__ = "_pytest.mark"
[docs]def testing_boolean_values(
extra_truthy: Sequence = (),
extra_falsy: Sequence = (),
ratio: float = 1,
) -> MarkDecorator:
"""
Returns a `pytest.mark.parametrize <https://docs.pytest.org/en/stable/parametrize.html>`__
decorator which provides a list of strings, integers and booleans, and the boolean representations of them.
The parametrized arguments are ``boolean_string`` for the input value,
and ``expected_boolean`` for the expected output.
Optionally, a random selection of the values can be returned using the ``ratio`` argument.
:param extra_truthy: Additional values to treat as :py:obj:`True`.
:param extra_falsy: Additional values to treat as :py:obj:`False`.
:param ratio: The ratio of the number of values to select to the total number of values.
""" # noqa: D400
truthy = generate_truthy_values(extra_truthy, ratio)
falsy = generate_falsy_values(extra_falsy, ratio)
boolean_strings = [ # pylint: disable=use-tuple-over-list
*itertools.zip_longest(truthy, [], fillvalue=True),
*itertools.zip_longest(falsy, [], fillvalue=False),
]
return pytest.mark.parametrize("boolean_string, expected_boolean", boolean_strings)
[docs]def whitespace_perms(ratio: float = 0.5) -> MarkDecorator:
r"""
Returns a `pytest.mark.parametrize <https://docs.pytest.org/en/stable/parametrize.html>`__
decorator which provides permutations of whitespace.
For this function whitespace is only ``␣\n\t\r``.
Not all permutations are returned, as there are a lot of them;
instead a random selection of the permutations is returned.
By default ½ of the permutations are returned, but this can be configured using the ``ratio`` argument.
The single parametrized argument is ``char``.
:param ratio: The ratio of the number of permutations to select to the total number of permutations.
""" # noqa: D400
perms = whitespace_perms_list()
return pytest.mark.parametrize("char", random.sample(perms, int(len(perms) * ratio)))
[docs]def count(stop: int, start: int = 0, step: int = 1) -> MarkDecorator:
"""
Returns a `pytest.mark.parametrize <https://docs.pytest.org/en/stable/parametrize.html>`__
decorator which provides a list of numbers between ``start`` and ``stop`` with an interval of ``step``.
The single parametrized argument is ``count``.
:param stop: The stop value passed to :class:`range`.
:param start: The start value passed to :class:`range`.
:param step: The step passed to :class:`range`.
""" # noqa: D400
return pytest.mark.parametrize("count", range(start, stop, step))
@overload
def param(
*values: object,
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
id: Optional[str] = ..., # noqa: A002 # pylint: disable=redefined-builtin
) -> ParameterSet: ...
@overload
def param(
*values: object,
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
idx: Optional[int],
) -> ParameterSet: ...
@overload
def param(
*values: _T,
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
key: Optional[Callable[[Tuple[_T, ...]], str]],
) -> ParameterSet: ...
[docs]def param(
*values: _T,
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
id: Optional[str] = None, # noqa: A002 # pylint: disable=redefined-builtin
idx: Optional[int] = None,
key: Optional[Callable[[Tuple[_T, ...]], str]] = None,
) -> ParameterSet:
r"""
Specify a parameter in `pytest.mark.parametrize <https://docs.pytest.org/en/stable/parametrize.html>`__
calls or :ref:`parametrized fixtures <fixture-parametrize-marks>`.
**Examples:**
.. code-block:: python
@pytest.mark.parametrize("test_input, expected", [
("3+5", 8),
param("6*9", 42, marks=pytest.mark.xfail),
param("2**2", 4, idx=0),
param("3**2", 9, id="3^2"),
param("sqrt(9)", 3, key=itemgetter(0)),
])
def test_eval(test_input, expected):
assert eval (test_input) == expected
.. versionadded:: 0.4.0
:param \*values: Variable args of the values of the parameter set, in order.
:param marks: A single mark or a list of marks to be applied to this parameter set.
:param id: The id to attribute to this parameter set.
:param idx: The index of the value in ``*values`` to use as the id.
:param key: A callable which is given ``values`` (as a :class:`tuple`) and returns the value to use as the id.
:rtype:
.. clearpage::
""" # noqa: D400
if len([x for x in (id, idx, key) if x is not None]) > 1:
raise ValueError("'id', 'idx' and 'key' are mutually exclusive.")
if idx is not None:
# pytest will catch the type error later on
id = cast(str, values[idx]) # noqa: A001 # pylint: disable=redefined-builtin
elif key is not None:
id = key(values) # noqa: A001 # pylint: disable=redefined-builtin
return ParameterSet.param(*values, marks=marks, id=id)
[docs]def parametrized_versions(
*versions: Union[str, float, Tuple[int, ...]],
reasons: Union[str, Iterable[Optional[str]]] = (),
) -> List[ParameterSet]:
r"""
Return a list of parametrized version numbers.
**Examples:**
.. code-block:: python
@pytest.mark.parametrize(
"version",
parametrized_versions(
3.6,
3.7,
3.8,
reason="Output differs on each version.",
),
)
def test_something(version: str):
pass
.. code-block:: python
@pytest.fixture(
params=parametrized_versions(
3.6,
3.7,
3.8,
reason="Output differs on each version.",
),
)
def version(request):
return request.param
def test_something(version: str):
pass
.. versionadded:: 0.4.0
:param \*versions: The Python versions to parametrize.
:param reasons: The reasons to use when skipping versions.
Either a string value to use for all versions,
or a list of values which correspond to ``*versions``.
"""
version_list = list(versions)
params = []
if isinstance(reasons, str):
reasons = [reasons] * len(version_list)
else:
reasons = extend_with_none(reasons, len(version_list))
for version, reason in zip(version_list, reasons):
version_ = _make_version(version)
the_param = pytest.param(
f"{version_.major}.{version_.minor}",
marks=only_version(version_, reason=reason),
)
params.append(the_param)
return params