Skip to content

Commit a849c17

Browse files
authored
Merge pull request #4212 from Zac-HD/warn-collect-hiddendir
warn on suspicious pytest-collection of `.hypothesis` directory
2 parents e6f4519 + 8be3633 commit a849c17

File tree

9 files changed

+85
-33
lines changed

9 files changed

+85
-33
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
RELEASE_TYPE: patch
2+
3+
Our pytest plugin now emits a warning if you set Pytest's ``norecursedirs``
4+
config option in such a way that the ``.hypothesis`` directory would be
5+
searched for tests. This reliably indicates that you've made a mistake
6+
which slows down test collection, usually assuming that your configuration
7+
extends the set of ignored patterns when it actually replaces them.
8+
(:issue:`4200`)

hypothesis-python/src/_hypothesis_pytestplugin.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import os
2525
import sys
2626
import warnings
27+
from fnmatch import fnmatch
2728
from inspect import signature
2829

2930
import _hypothesis_globals
@@ -444,6 +445,26 @@ def _ban_given_call(self, function):
444445
_orig_call = fixtures.FixtureFunctionMarker.__call__
445446
fixtures.FixtureFunctionMarker.__call__ = _ban_given_call # type: ignore
446447

448+
if int(pytest.__version__.split(".")[0]) >= 7: # pragma: no branch
449+
# Hook has had this signature since Pytest 7.0, so skip on older versions
450+
451+
def pytest_ignore_collect(collection_path, config):
452+
# Detect, warn about, and mititgate certain misconfigurations;
453+
# this is mostly educational but can also speed up collection.
454+
if (
455+
(name := collection_path.name) == ".hypothesis"
456+
and collection_path.is_dir()
457+
and not any(fnmatch(name, p) for p in config.getini("norecursedirs"))
458+
):
459+
warnings.warn(
460+
"Skipping collection of '.hypothesis' directory - this usually "
461+
"means you've explicitly set the `norecursedirs` pytest config "
462+
"option, replacing rather than extending the default ignores.",
463+
stacklevel=1,
464+
)
465+
return True
466+
return None # let other hooks decide
467+
447468

448469
def load():
449470
"""Required for `pluggy` to load a plugin from setuptools entrypoints."""

hypothesis-python/src/hypothesis/internal/conjecture/junkdrawer.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -269,19 +269,6 @@ def __underlying_index(self, i: int) -> int:
269269
return i
270270

271271

272-
def clamp(lower: float, value: float, upper: float) -> float:
273-
"""Given a value and lower/upper bounds, 'clamp' the value so that
274-
it satisfies lower <= value <= upper."""
275-
# this seems pointless (and is for integers), but handles the -0.0/0.0 case.
276-
# clamp(-1, 0.0, -0.0) violates the bounds by returning 0.0, since min(0.0, -0.0)
277-
# takes the first value of 0.0.
278-
if value == lower:
279-
return lower
280-
if value == upper:
281-
return upper
282-
return max(lower, min(value, upper))
283-
284-
285272
def swap(ls: LazySequenceCopy, i: int, j: int) -> None:
286273
"""Swap the elements ls[i], ls[j]."""
287274
if i == j:

hypothesis-python/src/hypothesis/internal/floats.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
from sys import float_info
1414
from typing import TYPE_CHECKING, Callable, Literal, SupportsFloat, Union
1515

16-
from hypothesis.internal.conjecture.junkdrawer import clamp
17-
1816
if TYPE_CHECKING:
1917
from typing import TypeAlias
2018
else:
@@ -208,6 +206,17 @@ def sign_aware_lte(x: float, y: float) -> bool:
208206
return x <= y
209207

210208

209+
def clamp(lower: float, value: float, upper: float) -> float:
210+
"""Given a value and lower/upper bounds, 'clamp' the value so that
211+
it satisfies lower <= value <= upper. NaN is mapped to lower."""
212+
# this seems pointless (and is for integers), but handles the -0.0/0.0 case.
213+
if not sign_aware_lte(lower, value):
214+
return lower
215+
if not sign_aware_lte(value, upper):
216+
return upper
217+
return value
218+
219+
211220
SMALLEST_SUBNORMAL = next_up(0.0)
212221
SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa
213222
assert math.isnan(SIGNALING_NAN)

hypothesis-python/src/hypothesis/strategies/_internal/utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717

1818
from hypothesis.internal.cache import LRUReusedCache
1919
from hypothesis.internal.compat import dataclass_asdict
20-
from hypothesis.internal.conjecture.junkdrawer import clamp
21-
from hypothesis.internal.floats import float_to_int
20+
from hypothesis.internal.floats import clamp, float_to_int
2221
from hypothesis.internal.reflection import proxies
2322
from hypothesis.vendor.pretty import pretty
2423

hypothesis-python/tests/conjecture/test_junkdrawer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@
2020
NotFound,
2121
SelfOrganisingList,
2222
binary_search,
23-
clamp,
2423
endswith,
2524
replace_all,
2625
stack_depth_of_caller,
2726
startswith,
2827
)
29-
from hypothesis.internal.floats import float_to_int, sign_aware_lte
28+
from hypothesis.internal.floats import clamp, float_to_int, sign_aware_lte
3029

3130

3231
def test_out_of_range():
@@ -77,6 +76,8 @@ def clamp_inputs(draw):
7776
@example((5, 1, 10))
7877
@example((-5, 0.0, -0.0))
7978
@example((0.0, -0.0, 5))
79+
@example((-0.0, 0.0, 0.0))
80+
@example((-0.0, -0.0, 0.0))
8081
@given(clamp_inputs())
8182
def test_clamp(input):
8283
lower, value, upper = input

hypothesis-python/tests/cover/test_float_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def test_next_float_equal(func, val):
7070
@example(float_kw(1, 4, smallest_nonzero_magnitude=4), -4)
7171
@example(float_kw(1, 4, smallest_nonzero_magnitude=4), -5)
7272
@example(float_kw(1, 4, smallest_nonzero_magnitude=4), -6)
73-
@example(float_kw(-5e-324, -0.0, smallest_nonzero_magnitude=5e-324), 3.0)
73+
@example(float_kw(-5e-324, -0.0), 3.0)
74+
@example(float_kw(0.0, 0.0), -0.0)
75+
@example(float_kw(-0.0, -0.0), 0.0)
7476
@given(float_kwargs(), st.floats())
7577
def test_float_clamper(kwargs, input_value):
7678
min_value = kwargs["min_value"]

hypothesis-python/tests/nocover/test_strategy_state.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from hypothesis import Verbosity, assume, settings
1616
from hypothesis.database import InMemoryExampleDatabase
1717
from hypothesis.internal.compat import PYPY
18-
from hypothesis.internal.floats import float_to_int, int_to_float, is_negative
18+
from hypothesis.internal.floats import clamp, float_to_int, int_to_float, is_negative
1919
from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
2020
from hypothesis.strategies import (
2121
binary,
@@ -37,18 +37,6 @@
3737
AVERAGE_LIST_LENGTH = 2
3838

3939

40-
def clamp(lower, value, upper):
41-
"""Given a value and optional lower/upper bounds, 'clamp' the value so that
42-
it satisfies lower <= value <= upper."""
43-
if (lower is not None) and (upper is not None) and (lower > upper):
44-
raise ValueError(f"Cannot clamp with lower > upper: {lower!r} > {upper!r}")
45-
if lower is not None:
46-
value = max(lower, value)
47-
if upper is not None:
48-
value = min(value, upper)
49-
return value
50-
51-
5240
class HypothesisSpec(RuleBasedStateMachine):
5341
def __init__(self):
5442
super().__init__()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# This file is part of Hypothesis, which may be found at
2+
# https://github.com/HypothesisWorks/hypothesis/
3+
#
4+
# Copyright the Hypothesis Authors.
5+
# Individual contributors are listed in AUTHORS.rst and the git log.
6+
#
7+
# This Source Code Form is subject to the terms of the Mozilla Public License,
8+
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9+
# obtain one at https://mozilla.org/MPL/2.0/.
10+
11+
import pytest
12+
13+
pytest_plugins = "pytester"
14+
15+
INI = """
16+
[pytest]
17+
norecursedirs = .svn tmp whatever*
18+
"""
19+
20+
TEST_SCRIPT = """
21+
def test_noop():
22+
pass
23+
"""
24+
25+
26+
@pytest.mark.skipif(int(pytest.__version__.split(".")[0]) < 7, reason="hook is new")
27+
def test_collection_warning(pytester):
28+
pytester.mkdir(".hypothesis")
29+
pytester.path.joinpath("pytest.ini").write_text(INI, encoding="utf-8")
30+
pytester.path.joinpath("test_ok.py").write_text(TEST_SCRIPT, encoding="utf-8")
31+
pytester.path.joinpath(".hypothesis/test_bad.py").write_text(
32+
TEST_SCRIPT.replace("pass", "raise Exception"), encoding="utf-8"
33+
)
34+
35+
result = pytester.runpytest_subprocess()
36+
result.assert_outcomes(passed=1, warnings=1)
37+
assert "Skipping collection of '.hypothesis'" in "\n".join(result.outlines)

0 commit comments

Comments
 (0)