doctest: Add +NUMBER option to ignore irrelevant floating-point differences
When enabled, floating-point numbers only need to match as far as the
precision you have written in the expected doctest output. This avoids
false positives caused by limited floating-point precision, like this:
Expected:
0.233
Got:
0.23300000000000001
This is inspired by Sébastien Boisgérault's [numtest] but the
implementation is a bit different:
* This implementation edits the literals that are in the "got"
string (the actual output from the expression being tested), and then
proceeds to compare the strings literally. This is similar to pytest's
existing ALLOW_UNICODE and ALLOW_BYTES implementation.
* This implementation only compares floats against floats, not ints
against floats. That is, the following doctest will fail with pytest
whereas it would pass with numtest:
>>> math.py # doctest: +NUMBER
3
This behaviour should be less surprising (less false negatives) when
you enable NUMBER globally in pytest.ini.
Advantages of this implementation compared to numtest:
* Doesn't require `import numtest` at the top level of the file.
* Works with pytest (if you try to use pytest & numtest together, pytest
raises "TypeError: unbound method check_output() must be called with
NumTestOutputChecker instance as first argument (got
LiteralsOutputChecker instance instead)").
* Works with Python 3.
[numtest]: https://github.com/boisgera/numtest
This commit is contained in:
@@ -3,6 +3,7 @@ import textwrap
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import MODULE_NOT_FOUND_ERROR
|
||||
from _pytest.doctest import _get_checker
|
||||
from _pytest.doctest import _is_mocked
|
||||
from _pytest.doctest import _patch_unwrap_mock_aware
|
||||
from _pytest.doctest import DoctestItem
|
||||
@@ -838,6 +839,149 @@ class TestLiterals:
|
||||
reprec = testdir.inline_run()
|
||||
reprec.assertoutcome(failed=1)
|
||||
|
||||
def test_number_re(self):
|
||||
for s in [
|
||||
"1.",
|
||||
"+1.",
|
||||
"-1.",
|
||||
".1",
|
||||
"+.1",
|
||||
"-.1",
|
||||
"0.1",
|
||||
"+0.1",
|
||||
"-0.1",
|
||||
"1e5",
|
||||
"+1e5",
|
||||
"1e+5",
|
||||
"+1e+5",
|
||||
"1e-5",
|
||||
"+1e-5",
|
||||
"-1e-5",
|
||||
"1.2e3",
|
||||
"-1.2e-3",
|
||||
]:
|
||||
print(s)
|
||||
m = _get_checker()._number_re.match(s)
|
||||
assert m is not None
|
||||
assert float(m.group()) == pytest.approx(float(s))
|
||||
for s in ["1", "abc"]:
|
||||
print(s)
|
||||
assert _get_checker()._number_re.match(s) is None
|
||||
|
||||
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
||||
def test_number_precision(self, testdir, config_mode):
|
||||
"""Test the NUMBER option."""
|
||||
if config_mode == "ini":
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
doctest_optionflags = NUMBER
|
||||
"""
|
||||
)
|
||||
comment = ""
|
||||
else:
|
||||
comment = "#doctest: +NUMBER"
|
||||
|
||||
testdir.maketxtfile(
|
||||
test_doc="""
|
||||
|
||||
Scalars:
|
||||
|
||||
>>> import math
|
||||
>>> math.pi {comment}
|
||||
3.141592653589793
|
||||
>>> math.pi {comment}
|
||||
3.1416
|
||||
>>> math.pi {comment}
|
||||
3.14
|
||||
>>> -math.pi {comment}
|
||||
-3.14
|
||||
>>> math.pi {comment}
|
||||
3.
|
||||
>>> 3. {comment}
|
||||
3.0
|
||||
>>> 3. {comment}
|
||||
3.
|
||||
>>> 3. {comment}
|
||||
3.01
|
||||
>>> 3. {comment}
|
||||
2.99
|
||||
>>> .299 {comment}
|
||||
.3
|
||||
>>> .301 {comment}
|
||||
.3
|
||||
>>> 951. {comment}
|
||||
1e3
|
||||
>>> 1049. {comment}
|
||||
1e3
|
||||
>>> -1049. {comment}
|
||||
-1e3
|
||||
>>> 1e3 {comment}
|
||||
1e3
|
||||
>>> 1e3 {comment}
|
||||
1000.
|
||||
|
||||
Lists:
|
||||
|
||||
>>> [3.1415, 0.097, 13.1, 7, 8.22222e5, 0.598e-2] {comment}
|
||||
[3.14, 0.1, 13., 7, 8.22e5, 6.0e-3]
|
||||
>>> [[0.333, 0.667], [0.999, 1.333]] {comment}
|
||||
[[0.33, 0.667], [0.999, 1.333]]
|
||||
>>> [[[0.101]]] {comment}
|
||||
[[[0.1]]]
|
||||
|
||||
Doesn't barf on non-numbers:
|
||||
|
||||
>>> 'abc' {comment}
|
||||
'abc'
|
||||
>>> None {comment}
|
||||
""".format(
|
||||
comment=comment
|
||||
)
|
||||
)
|
||||
reprec = testdir.inline_run()
|
||||
reprec.assertoutcome(passed=1)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expression,output",
|
||||
[
|
||||
# ints shouldn't match floats:
|
||||
("3.0", "3"),
|
||||
("3e0", "3"),
|
||||
("1e3", "1000"),
|
||||
("3", "3.0"),
|
||||
# Rounding:
|
||||
("3.1", "3.0"),
|
||||
("3.1", "3.2"),
|
||||
("3.1", "4.0"),
|
||||
("8.22e5", "810000.0"),
|
||||
# Only the actual output is rounded up, not the expected output:
|
||||
("3.0", "2.99"),
|
||||
("1e3", "999"),
|
||||
],
|
||||
)
|
||||
def test_number_non_matches(self, testdir, expression, output):
|
||||
testdir.maketxtfile(
|
||||
test_doc="""
|
||||
>>> {expression} #doctest: +NUMBER
|
||||
{output}
|
||||
"""
|
||||
)
|
||||
reprec = testdir.inline_run()
|
||||
reprec.assertoutcome(passed=0, failed=1)
|
||||
|
||||
def test_number_and_allow_unicode(self, testdir):
|
||||
testdir.maketxtfile(
|
||||
test_doc="""
|
||||
>>> from collections import namedtuple
|
||||
>>> T = namedtuple('T', 'a b c')
|
||||
>>> T(a=0.2330000001, b=u'str', c=b'bytes') # doctest: +ALLOW_UNICODE, +ALLOW_BYTES, +NUMBER
|
||||
T(a=0.233, b=u'str', c='bytes')
|
||||
"""
|
||||
)
|
||||
reprec = testdir.inline_run()
|
||||
reprec.assertoutcome(passed=1)
|
||||
|
||||
|
||||
class TestDoctestSkips:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user