Skip to content

Commit 5221dbd

Browse files
authored
Merge pull request #4065 from Zac-HD/numeric-pprinter
Improve numeric pprinting
2 parents 0291b05 + c50ffd4 commit 5221dbd

File tree

9 files changed

+82
-49
lines changed

9 files changed

+82
-49
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch improves our pretty-printer for unusual numbers.
4+
5+
- Signalling NaNs are now represented by using the :mod:`struct` module
6+
to show the exact value by converting from a hexadecimal integer
7+
8+
- CPython `limits integer-to-string conversions
9+
<https://docs.python.org/3/library/stdtypes.html#integer-string-conversion-length-limitation>`__
10+
to mitigate DDOS attacks. We now use hexadecimal for very large
11+
integers, and include underscore separators for integers with ten
12+
or more digits.

hypothesis-python/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def local_file(name):
6060
"pytest": ["pytest>=4.6"],
6161
"dpcontracts": ["dpcontracts>=0.4"],
6262
"redis": ["redis>=3.0.0"],
63-
"crosshair": ["hypothesis-crosshair>=0.0.9", "crosshair-tool>=0.0.63"],
63+
"crosshair": ["hypothesis-crosshair>=0.0.11", "crosshair-tool>=0.0.65"],
6464
# zoneinfo is an odd one: every dependency is conditional, because they're
6565
# only necessary on old versions of Python or Windows systems or emscripten.
6666
"zoneinfo": [

hypothesis-python/src/hypothesis/vendor/pretty.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,6 @@ def __init__(self, output=None, *, context=None):
143143
self.group_queue = GroupQueue(root_group)
144144
self.indentation = 0
145145

146-
self.snans = 0
147-
148146
self.stack = []
149147
self.singleton_pprinters = {}
150148
self.type_pprinters = {}
@@ -358,12 +356,6 @@ def _enumerate(self, seq):
358356

359357
def flush(self):
360358
"""Flush data that is left in the buffer."""
361-
if self.snans:
362-
# Reset self.snans *before* calling breakable(), which might flush()
363-
snans = self.snans
364-
self.snans = 0
365-
self.breakable(" ")
366-
self.text(f"# Saw {snans} signaling NaN" + "s" * (snans > 1))
367359
for data in self.buffer:
368360
self.output_width += data.output(self.output, self.output_width)
369361
self.buffer.clear()
@@ -747,19 +739,31 @@ def _exception_pprint(obj, p, cycle):
747739
p.pretty(arg)
748740

749741

742+
def _repr_integer(obj, p, cycle):
743+
if abs(obj) < 1_000_000_000:
744+
p.text(repr(obj))
745+
elif abs(obj) < 10**640:
746+
# add underscores for integers over ten decimal digits
747+
p.text(f"{obj:#_d}")
748+
else:
749+
# for very very large integers, use hex because power-of-two bases are cheaper
750+
# https://docs.python.org/3/library/stdtypes.html#integer-string-conversion-length-limitation
751+
p.text(f"{obj:#_x}")
752+
753+
750754
def _repr_float_counting_nans(obj, p, cycle):
751-
if isnan(obj) and hasattr(p, "snans"):
755+
if isnan(obj):
752756
if struct.pack("!d", abs(obj)) != struct.pack("!d", float("nan")):
753-
p.snans += 1
754-
if copysign(1.0, obj) == -1.0:
755-
p.text("-nan")
756-
return
757+
show = hex(*struct.unpack("Q", struct.pack("d", obj)))
758+
return p.text(f"struct.unpack('d', struct.pack('Q', {show}))[0]")
759+
elif copysign(1.0, obj) == -1.0:
760+
return p.text("-nan")
757761
p.text(repr(obj))
758762

759763

760764
#: printers for builtin types
761765
_type_pprinters = {
762-
int: _repr_pprint,
766+
int: _repr_integer,
763767
float: _repr_float_counting_nans,
764768
str: _repr_pprint,
765769
tuple: _seq_pprinter_factory("(", ")", tuple),

hypothesis-python/tests/cover/test_cache_implementation.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,18 @@ def on_access(self, key, value, score):
5656

5757

5858
@st.composite
59-
def write_pattern(draw, min_size=0):
60-
keys = draw(st.lists(st.integers(0, 1000), unique=True, min_size=1))
59+
def write_pattern(draw, min_distinct_keys=0):
60+
keys = draw(
61+
st.lists(st.integers(0, 1000), unique=True, min_size=max(min_distinct_keys, 1))
62+
)
6163
values = draw(st.lists(st.integers(), unique=True, min_size=1))
62-
return draw(
63-
st.lists(
64-
st.tuples(st.sampled_from(keys), st.sampled_from(values)), min_size=min_size
65-
)
64+
s = st.lists(
65+
st.tuples(st.sampled_from(keys), st.sampled_from(values)),
66+
min_size=min_distinct_keys,
6667
)
68+
if min_distinct_keys > 0:
69+
s = s.filter(lambda ls: len({k for k, _ in ls}) >= min_distinct_keys)
70+
return draw(s)
6771

6872

6973
class ValueScored(GenericCache):
@@ -111,8 +115,12 @@ def test_behaves_like_a_dict_with_losses(implementation, writes, size):
111115
assert len(target) <= min(len(model), size)
112116

113117

114-
@settings(suppress_health_check=[HealthCheck.too_slow], deadline=None)
115-
@given(write_pattern(min_size=2), st.data())
118+
@settings(
119+
suppress_health_check={HealthCheck.too_slow}
120+
| set(settings.get_profile(settings._current_profile).suppress_health_check),
121+
deadline=None,
122+
)
123+
@given(write_pattern(min_distinct_keys=2), st.data())
116124
def test_always_evicts_the_lowest_scoring_value(writes, data):
117125
scores = {}
118126

hypothesis-python/tests/cover/test_explicit_examples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def test(a):
224224

225225
@fails_with(DeadlineExceeded)
226226
@example(10)
227-
@settings(phases=[Phase.explicit])
227+
@settings(phases=[Phase.explicit], deadline=1)
228228
@given(integers())
229229
def test(x):
230230
time.sleep(10)

hypothesis-python/tests/cover/test_float_nastiness.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,10 @@
4141
(-sys.float_info.max, sys.float_info.max),
4242
],
4343
)
44-
def test_floats_are_in_range(lower, upper):
45-
@given(st.floats(lower, upper))
46-
def test_is_in_range(t):
47-
assert lower <= t <= upper
48-
49-
test_is_in_range()
44+
@given(data=st.data())
45+
def test_floats_are_in_range(data, lower, upper):
46+
t = data.draw(st.floats(lower, upper))
47+
assert lower <= t <= upper
5048

5149

5250
@pytest.mark.parametrize("sign", [-1, 1])

hypothesis-python/tests/cover/test_health_checks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def test2(x):
9191

9292
def test_filtering_everything_fails_a_health_check():
9393
@given(st.integers().filter(lambda x: False))
94-
@settings(database=None)
94+
@settings(database=None, suppress_health_check=())
9595
def test(x):
9696
pass
9797

hypothesis-python/tests/cover/test_pretty.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"""
4949

5050
import re
51+
import struct
5152
import warnings
5253
from collections import Counter, OrderedDict, defaultdict, deque
5354
from enum import Enum, Flag
@@ -58,6 +59,7 @@
5859
from hypothesis import given, strategies as st
5960
from hypothesis.control import current_build_context
6061
from hypothesis.internal.compat import PYPY
62+
from hypothesis.internal.conjecture.floats import float_to_lex
6163
from hypothesis.internal.floats import SIGNALING_NAN
6264
from hypothesis.vendor import pretty
6365

@@ -603,13 +605,15 @@ def test_breakable_at_group_boundary():
603605
[
604606
(float("nan"), "nan"),
605607
(-float("nan"), "-nan"),
606-
(SIGNALING_NAN, "nan # Saw 1 signaling NaN"),
607-
(-SIGNALING_NAN, "-nan # Saw 1 signaling NaN"),
608-
((SIGNALING_NAN, SIGNALING_NAN), "(nan, nan) # Saw 2 signaling NaNs"),
608+
(SIGNALING_NAN, "struct.unpack('d', struct.pack('Q', 0x7ff8000000000001))[0]"),
609+
(-SIGNALING_NAN, "struct.unpack('d', struct.pack('Q', 0xfff8000000000001))[0]"),
609610
],
610611
)
611612
def test_nan_reprs(obj, rep):
612613
assert pretty.pretty(obj) == rep
614+
assert float_to_lex(obj) == float_to_lex(
615+
eval(rep, {"struct": struct, "nan": float("nan")})
616+
)
613617

614618

615619
def _repr_call(*args, **kwargs):
@@ -739,3 +743,18 @@ def test_pprint_map_with_cycle(data):
739743
p = pretty.RepresentationPrinter(context=current_build_context())
740744
p.pretty(x)
741745
assert p.getvalue() == "ValidSyntaxRepr(...)"
746+
747+
748+
def test_pprint_large_integers():
749+
p = pretty.RepresentationPrinter()
750+
p.pretty(1234567890)
751+
assert p.getvalue() == "1_234_567_890"
752+
753+
754+
def test_pprint_extremely_large_integers():
755+
x = 10**5000 # repr fails with ddos error
756+
p = pretty.RepresentationPrinter()
757+
p.pretty(x)
758+
got = p.getvalue()
759+
assert got == f"{x:#_x}" # hexadecimal with underscores
760+
assert eval(got) == x

hypothesis-python/tests/nocover/test_recursive.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,14 @@ def breadth(x):
7373

7474

7575
def test_drawing_many_near_boundary():
76-
target = 4
77-
78-
ls = minimal(
79-
st.lists(
80-
st.recursive(
81-
st.booleans(),
82-
lambda x: st.lists(
83-
x, min_size=2 * (target - 1), max_size=2 * target
84-
).map(tuple),
85-
max_leaves=2 * target - 1,
86-
)
87-
),
88-
lambda x: len(set(x)) >= target,
89-
timeout_after=None,
76+
size = 4
77+
elems = st.recursive(
78+
st.booleans(),
79+
lambda x: st.lists(x, min_size=2 * (size - 1), max_size=2 * size).map(tuple),
80+
max_leaves=2 * size - 1,
9081
)
91-
assert len(ls) == target
82+
ls = minimal(st.lists(elems), lambda x: len(set(x)) >= size, timeout_after=None)
83+
assert len(ls) == size
9284

9385

9486
def test_can_use_recursive_data_in_sets():

0 commit comments

Comments
 (0)