Skip to content

Commit

Permalink
Merge pull request #3158 from Zac-HD/pprint-weird-nans
Browse files Browse the repository at this point in the history
Distinguish negative and signaling NaNs in our pretty-printer
  • Loading branch information
Zac-HD authored Nov 21, 2021
2 parents 679635c + e060521 commit 6b0dc30
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 32 deletions.
10 changes: 10 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
RELEASE_TYPE: minor

Did you know that of the 2\ :superscript:`64` possible floating-point numbers,
2\ :superscript:`53` of them are ``nan`` - and Python prints them all the same way?

While nans *usually* have all zeros in the sign bit and mantissa, this
`isn't always true <https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations>`__,
and :wikipedia:`'signaling' nans might trap or error <https://en.wikipedia.org/wiki/NaN#Signaling_NaN>`.
To help distinguish such errors in e.g. CI logs, Hypothesis now prints ``-nan`` for
negative nans, and adds a comment like ``# Saw 3 signaling NaNs`` if applicable.
4 changes: 2 additions & 2 deletions hypothesis-python/docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ like ``s.map(lambda x: x)`` and ``lists().filter(len)`` more efficient
6.24.6 - 2021-11-18
-------------------

This patch makes :func:`hypothesis.strategies.floats` generate
This patch makes :func:`~hypothesis.strategies.floats` generate
:wikipedia:`"subnormal" floating point numbers <Subnormal_number>`
more often, as these rare values can have strange interactions with
`unsafe compiler optimisations like -ffast-math
Expand All @@ -47,7 +47,7 @@ more often, as these rare values can have strange interactions with
6.24.5 - 2021-11-16
-------------------

This patch fixes a rare internal error in the :func:`hypothesis.strategies.datetimes`
This patch fixes a rare internal error in the :func:`~hypothesis.strategies.datetimes`
strategy, where the implementation of ``allow_imaginary=False`` crashed when checking
a time during the skipped hour of a DST transition *if* the DST offset is negative -
only true of ``Europe/Dublin``, who we presume have their reasons - and the ``tzinfo``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ def integers(
return IntegersStrategy(min_value, max_value)


SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa
assert math.isnan(SIGNALING_NAN) and math.copysign(1, SIGNALING_NAN) == 1

NASTY_FLOATS = sorted(
[
0.0,
Expand All @@ -170,7 +173,8 @@ def integers(
]
+ [2.0 ** -n for n in (24, 14, 149, 126)] # minimum (sub)normals for float16,32
+ [float_info.min / n for n in (2, 10, 1000, 100_000)] # subnormal in float64
+ [math.inf, math.nan] * 5,
+ [math.inf, math.nan] * 5
+ [SIGNALING_NAN],
key=flt.float_to_lex,
)
NASTY_FLOATS = list(map(float, NASTY_FLOATS))
Expand Down
40 changes: 21 additions & 19 deletions hypothesis-python/src/hypothesis/vendor/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
This module is based on ruby's `prettyprint.rb` library by `Tanaka Akira`.
Example Usage
-------------
To directly print the representation of an object use `pprint`::
from pretty import pprint
pprint(complex_object)
To get a string of the output use `pretty`::
from pretty import pretty
string = pretty(complex_object)
Expand Down Expand Up @@ -73,15 +70,15 @@ def _repr_pretty_(self, p, cycle):
import datetime
import platform
import re
import sys
import struct
import types
from collections import deque
from contextlib import contextmanager
from io import StringIO
from math import copysign, isnan

__all__ = [
"pretty",
"pprint",
"PrettyPrinter",
"RepresentationPrinter",
"for_type_by_name",
Expand Down Expand Up @@ -120,19 +117,6 @@ def pretty(
return stream.getvalue()


def pprint(
obj, verbose=False, max_width=79, newline="\n", max_seq_length=MAX_SEQ_LENGTH
):
"""Like `pretty` but print to stdout."""
printer = RepresentationPrinter(
sys.stdout, verbose, max_width, newline, max_seq_length=max_seq_length
)
printer.pretty(obj)
printer.flush()
sys.stdout.write(newline)
sys.stdout.flush()


class _PrettyPrinterBase:
@contextmanager
def indent(self, indent):
Expand Down Expand Up @@ -179,6 +163,8 @@ def __init__(
self.group_queue = GroupQueue(root_group)
self.indentation = 0

self.snans = 0

def _break_outer_groups(self):
while self.max_width < self.output_width + self.buffer_width:
group = self.group_queue.deq()
Expand Down Expand Up @@ -284,6 +270,12 @@ def end_group(self, dedent=0, close=""):

def flush(self):
"""Flush data that is left in the buffer."""
if self.snans:
# Reset self.snans *before* calling breakable(), which might flush()
snans = self.snans
self.snans = 0
self.breakable(" ")
self.text(f"# Saw {snans} signaling NaN" + "s" * (snans > 1))
for data in self.buffer:
self.output_width += data.output(self.output, self.output_width)
self.buffer.clear()
Expand Down Expand Up @@ -753,10 +745,20 @@ def _exception_pprint(obj, p, cycle):
p.end_group(step, ")")


def _repr_float_counting_nans(obj, p, cycle):
if isnan(obj) and hasattr(p, "snans"):
if struct.pack("!d", abs(obj)) != struct.pack("!d", float("nan")):
p.snans += 1
if copysign(1.0, obj) == -1.0:
p.text("-nan")
return
p.text(repr(obj))


#: printers for builtin types
_type_pprinters = {
int: _repr_pprint,
float: _repr_pprint,
float: _repr_float_counting_nans,
str: _repr_pprint,
tuple: _seq_pprinter_factory("(", ")", tuple),
list: _seq_pprinter_factory("[", "]", list),
Expand Down
24 changes: 15 additions & 9 deletions hypothesis-python/tests/cover/test_pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,9 @@
import pytest

from hypothesis.internal.compat import PYPY
from hypothesis.strategies._internal.numbers import SIGNALING_NAN
from hypothesis.vendor import pretty

from tests.common.utils import capture_out


class MyList:
def __init__(self, content):
Expand Down Expand Up @@ -546,13 +545,6 @@ def test_cyclic_set():
assert pretty.pretty(x) == "{{...}}"


def test_pprint():
t = {"hi": 1}
with capture_out() as o:
pretty.pprint(t)
assert o.getvalue().strip() == pretty.pretty(t)


class BigList(list):
def _repr_pretty_(self, printer, cycle):
if cycle:
Expand Down Expand Up @@ -631,3 +623,17 @@ def test_empty_printer():

def test_breakable_at_group_boundary():
assert "\n" in pretty.pretty([[], "000000"], max_width=5)


@pytest.mark.parametrize(
"obj, rep",
[
(float("nan"), "nan"),
(-float("nan"), "-nan"),
(SIGNALING_NAN, "nan # Saw 1 signaling NaN"),
(-SIGNALING_NAN, "-nan # Saw 1 signaling NaN"),
((SIGNALING_NAN, SIGNALING_NAN), "(nan, nan) # Saw 2 signaling NaNs"),
],
)
def test_nan_reprs(obj, rep):
assert pretty.pretty(obj) == rep
16 changes: 15 additions & 1 deletion hypothesis-python/tests/nocover/test_floating.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
import pytest

from hypothesis import HealthCheck, assume, given, settings
from hypothesis.internal.floats import next_down
from hypothesis.internal.floats import float_to_int, next_down
from hypothesis.strategies import data, floats, lists

from tests.common.debug import find_any
from tests.common.utils import fails

TRY_HARDER = settings(
Expand Down Expand Up @@ -161,3 +162,16 @@ def test_floats_are_in_range(x, y, data):

t = data.draw(floats(x, y))
assert x <= t <= y


@pytest.mark.parametrize("neg", [False, True])
@pytest.mark.parametrize("snan", [False, True])
def test_can_find_negative_and_signaling_nans(neg, snan):
find_any(
floats().filter(math.isnan),
lambda x: (
snan is (float_to_int(abs(x)) != float_to_int(float("nan")))
and neg is (math.copysign(1, x) == -1)
),
settings=TRY_HARDER,
)

0 comments on commit 6b0dc30

Please sign in to comment.