Skip to content

Commit

Permalink
Merge pull request #4212 from Zac-HD/warn-collect-hiddendir
Browse files Browse the repository at this point in the history
warn on suspicious pytest-collection of `.hypothesis` directory
  • Loading branch information
Zac-HD authored Dec 24, 2024
2 parents e6f4519 + 8be3633 commit a849c17
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 33 deletions.
8 changes: 8 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
RELEASE_TYPE: patch

Our pytest plugin now emits a warning if you set Pytest's ``norecursedirs``
config option in such a way that the ``.hypothesis`` directory would be
searched for tests. This reliably indicates that you've made a mistake
which slows down test collection, usually assuming that your configuration
extends the set of ignored patterns when it actually replaces them.
(:issue:`4200`)
21 changes: 21 additions & 0 deletions hypothesis-python/src/_hypothesis_pytestplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import os
import sys
import warnings
from fnmatch import fnmatch
from inspect import signature

import _hypothesis_globals
Expand Down Expand Up @@ -444,6 +445,26 @@ def _ban_given_call(self, function):
_orig_call = fixtures.FixtureFunctionMarker.__call__
fixtures.FixtureFunctionMarker.__call__ = _ban_given_call # type: ignore

if int(pytest.__version__.split(".")[0]) >= 7: # pragma: no branch
# Hook has had this signature since Pytest 7.0, so skip on older versions

def pytest_ignore_collect(collection_path, config):
# Detect, warn about, and mititgate certain misconfigurations;
# this is mostly educational but can also speed up collection.
if (
(name := collection_path.name) == ".hypothesis"
and collection_path.is_dir()
and not any(fnmatch(name, p) for p in config.getini("norecursedirs"))
):
warnings.warn(
"Skipping collection of '.hypothesis' directory - this usually "
"means you've explicitly set the `norecursedirs` pytest config "
"option, replacing rather than extending the default ignores.",
stacklevel=1,
)
return True
return None # let other hooks decide


def load():
"""Required for `pluggy` to load a plugin from setuptools entrypoints."""
13 changes: 0 additions & 13 deletions hypothesis-python/src/hypothesis/internal/conjecture/junkdrawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,19 +269,6 @@ def __underlying_index(self, i: int) -> int:
return i


def clamp(lower: float, value: float, upper: float) -> float:
"""Given a value and lower/upper bounds, 'clamp' the value so that
it satisfies lower <= value <= upper."""
# this seems pointless (and is for integers), but handles the -0.0/0.0 case.
# clamp(-1, 0.0, -0.0) violates the bounds by returning 0.0, since min(0.0, -0.0)
# takes the first value of 0.0.
if value == lower:
return lower
if value == upper:
return upper
return max(lower, min(value, upper))


def swap(ls: LazySequenceCopy, i: int, j: int) -> None:
"""Swap the elements ls[i], ls[j]."""
if i == j:
Expand Down
13 changes: 11 additions & 2 deletions hypothesis-python/src/hypothesis/internal/floats.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
from sys import float_info
from typing import TYPE_CHECKING, Callable, Literal, SupportsFloat, Union

from hypothesis.internal.conjecture.junkdrawer import clamp

if TYPE_CHECKING:
from typing import TypeAlias
else:
Expand Down Expand Up @@ -208,6 +206,17 @@ def sign_aware_lte(x: float, y: float) -> bool:
return x <= y


def clamp(lower: float, value: float, upper: float) -> float:
"""Given a value and lower/upper bounds, 'clamp' the value so that
it satisfies lower <= value <= upper. NaN is mapped to lower."""
# this seems pointless (and is for integers), but handles the -0.0/0.0 case.
if not sign_aware_lte(lower, value):
return lower
if not sign_aware_lte(value, upper):
return upper
return value


SMALLEST_SUBNORMAL = next_up(0.0)
SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa
assert math.isnan(SIGNALING_NAN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@

from hypothesis.internal.cache import LRUReusedCache
from hypothesis.internal.compat import dataclass_asdict
from hypothesis.internal.conjecture.junkdrawer import clamp
from hypothesis.internal.floats import float_to_int
from hypothesis.internal.floats import clamp, float_to_int
from hypothesis.internal.reflection import proxies
from hypothesis.vendor.pretty import pretty

Expand Down
5 changes: 3 additions & 2 deletions hypothesis-python/tests/conjecture/test_junkdrawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@
NotFound,
SelfOrganisingList,
binary_search,
clamp,
endswith,
replace_all,
stack_depth_of_caller,
startswith,
)
from hypothesis.internal.floats import float_to_int, sign_aware_lte
from hypothesis.internal.floats import clamp, float_to_int, sign_aware_lte


def test_out_of_range():
Expand Down Expand Up @@ -77,6 +76,8 @@ def clamp_inputs(draw):
@example((5, 1, 10))
@example((-5, 0.0, -0.0))
@example((0.0, -0.0, 5))
@example((-0.0, 0.0, 0.0))
@example((-0.0, -0.0, 0.0))
@given(clamp_inputs())
def test_clamp(input):
lower, value, upper = input
Expand Down
4 changes: 3 additions & 1 deletion hypothesis-python/tests/cover/test_float_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def test_next_float_equal(func, val):
@example(float_kw(1, 4, smallest_nonzero_magnitude=4), -4)
@example(float_kw(1, 4, smallest_nonzero_magnitude=4), -5)
@example(float_kw(1, 4, smallest_nonzero_magnitude=4), -6)
@example(float_kw(-5e-324, -0.0, smallest_nonzero_magnitude=5e-324), 3.0)
@example(float_kw(-5e-324, -0.0), 3.0)
@example(float_kw(0.0, 0.0), -0.0)
@example(float_kw(-0.0, -0.0), 0.0)
@given(float_kwargs(), st.floats())
def test_float_clamper(kwargs, input_value):
min_value = kwargs["min_value"]
Expand Down
14 changes: 1 addition & 13 deletions hypothesis-python/tests/nocover/test_strategy_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from hypothesis import Verbosity, assume, settings
from hypothesis.database import InMemoryExampleDatabase
from hypothesis.internal.compat import PYPY
from hypothesis.internal.floats import float_to_int, int_to_float, is_negative
from hypothesis.internal.floats import clamp, float_to_int, int_to_float, is_negative
from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
from hypothesis.strategies import (
binary,
Expand All @@ -37,18 +37,6 @@
AVERAGE_LIST_LENGTH = 2


def clamp(lower, value, upper):
"""Given a value and optional lower/upper bounds, 'clamp' the value so that
it satisfies lower <= value <= upper."""
if (lower is not None) and (upper is not None) and (lower > upper):
raise ValueError(f"Cannot clamp with lower > upper: {lower!r} > {upper!r}")
if lower is not None:
value = max(lower, value)
if upper is not None:
value = min(value, upper)
return value


class HypothesisSpec(RuleBasedStateMachine):
def __init__(self):
super().__init__()
Expand Down
37 changes: 37 additions & 0 deletions hypothesis-python/tests/pytest/test_collection_warning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

import pytest

pytest_plugins = "pytester"

INI = """
[pytest]
norecursedirs = .svn tmp whatever*
"""

TEST_SCRIPT = """
def test_noop():
pass
"""


@pytest.mark.skipif(int(pytest.__version__.split(".")[0]) < 7, reason="hook is new")
def test_collection_warning(pytester):
pytester.mkdir(".hypothesis")
pytester.path.joinpath("pytest.ini").write_text(INI, encoding="utf-8")
pytester.path.joinpath("test_ok.py").write_text(TEST_SCRIPT, encoding="utf-8")
pytester.path.joinpath(".hypothesis/test_bad.py").write_text(
TEST_SCRIPT.replace("pass", "raise Exception"), encoding="utf-8"
)

result = pytester.runpytest_subprocess()
result.assert_outcomes(passed=1, warnings=1)
assert "Skipping collection of '.hypothesis'" in "\n".join(result.outlines)

0 comments on commit a849c17

Please sign in to comment.