Skip to content

Commit ca1564a

Browse files
Merge pull request #43 from SaridakisStamatisChristos/codex/move-hypothesis-stub-and-add-tests
Conditionally use Hypothesis stub and expand regression tests
2 parents cbded1e + d245e98 commit ca1564a

File tree

6 files changed

+132
-161
lines changed

6 files changed

+132
-161
lines changed

hypothesis/__init__.py

Lines changed: 0 additions & 114 deletions
This file was deleted.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Minimal Hypothesis stub used only when the real package is unavailable."""
2+
from __future__ import annotations
3+
from enum import Enum
4+
from functools import wraps
5+
import inspect
6+
from itertools import product
7+
from typing import Any, Callable, Dict, List, Sequence, Tuple
8+
from . import strategies as strategies
9+
__all__ = ["HealthCheck","Phase","given","settings","strategies"]
10+
class HealthCheck(str, Enum):
11+
too_slow = "too_slow"
12+
filter_too_much = "filter_too_much"
13+
data_too_large = "data_too_large"
14+
class Phase(str, Enum):
15+
generate = "generate"
16+
shrink = "shrink"
17+
class settings:
18+
_profiles: Dict[str, Dict[str, Any]] = {"default": {}}
19+
_active_profile: Dict[str, Any] = _profiles["default"].copy()
20+
def __init__(self, **kwargs: Any) -> None:
21+
self.kwargs = kwargs
22+
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
23+
func._hypothesis_settings = self.kwargs # type: ignore[attr-defined]
24+
return func
25+
@classmethod
26+
def register_profile(cls, name: str, **kwargs: Any) -> None:
27+
cls._profiles[name] = dict(kwargs)
28+
@classmethod
29+
def load_profile(cls, name: str) -> None:
30+
cls._active_profile = cls._profiles.get(name, cls._profiles["default"]).copy()
31+
@classmethod
32+
def get_active_profile(cls) -> Dict[str, Any]:
33+
return cls._active_profile
34+
35+
def given(*strategies_args: strategies.Strategy, **strategies_kwargs: strategies.Strategy):
36+
if strategies_kwargs:
37+
raise NotImplementedError("keyword strategies are not supported in this stub")
38+
example_lists: List[Sequence[Any]] = [s.examples() for s in strategies_args]
39+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
40+
if not example_lists:
41+
return func
42+
example_product: List[Tuple[Any, ...]] = list(product(*example_lists))
43+
@wraps(func)
44+
def wrapper(*args: Any, **kwargs: Any) -> Any:
45+
for example in example_product:
46+
func(*example)
47+
wrapper.__signature__ = inspect.Signature() # type: ignore[attr-defined]
48+
return wrapper
49+
return decorator
Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
1-
"""Deterministic stand-ins for the handful of Hypothesis strategies we need."""
2-
1+
"""Deterministic stand-ins for a tiny subset of Hypothesis strategies."""
32
from __future__ import annotations
4-
53
from dataclasses import dataclass
64
from typing import Iterable, List, Sequence
7-
85
__all__ = ["Strategy", "sampled_from", "lists", "text"]
9-
10-
116
class Strategy:
12-
"""Base strategy API matching the expectations of :func:`hypothesis.given`."""
13-
14-
def examples(self) -> Sequence[object]: # pragma: no cover - interface only
7+
def examples(self) -> Sequence[object]:
158
raise NotImplementedError
16-
17-
189
@dataclass
1910
class SampledFromStrategy(Strategy):
2011
values: Sequence[object]
21-
2212
def __post_init__(self) -> None:
2313
if not self.values:
2414
self.values = [None]
25-
2615
def examples(self) -> Sequence[object]:
27-
# Provide a handful of distinct representatives while preserving order.
2816
unique: List[object] = []
2917
for candidate in (self.values[0], self.values[-1]):
3018
if candidate not in unique:
@@ -33,27 +21,16 @@ def examples(self) -> Sequence[object]:
3321
if midpoint not in unique:
3422
unique.append(midpoint)
3523
return tuple(unique)
36-
3724
def cycle(self, length: int, *, reverse: bool = False, offset: int = 0) -> List[object]:
38-
items = list(self.values[::-1] if reverse else self.values)
39-
if not items:
40-
items = [None]
41-
out: List[object] = []
42-
for i in range(length):
43-
out.append(items[(i + offset) % len(items)])
44-
return out
45-
46-
25+
items = list(self.values[::-1] if reverse else self.values) or [None]
26+
return [items[(i + offset) % len(items)] for i in range(length)]
4727
@dataclass
4828
class ListStrategy(Strategy):
4929
inner: Strategy
5030
min_size: int
5131
max_size: int
52-
5332
def examples(self) -> Sequence[object]:
54-
length = self.min_size
55-
if self.max_size < self.min_size:
56-
length = self.max_size
33+
length = min(max(self.min_size, 0), max(self.max_size, 0))
5734
cycle = getattr(self.inner, "cycle", None)
5835
if callable(cycle):
5936
base = cycle(length)
@@ -64,13 +41,11 @@ def examples(self) -> Sequence[object]:
6441
base = [(base_values[i % len(base_values)]) for i in range(length)]
6542
rotated = base[::-1]
6643
reversed_cycle = base_values[:length]
67-
6844
examples: List[List[object]] = [list(base)]
6945
if rotated and rotated != base:
7046
examples.append(list(rotated))
7147
if reversed_cycle and reversed_cycle not in examples:
7248
examples.append(list(reversed_cycle))
73-
7449
if self.max_size > self.min_size:
7550
alt_length = self.max_size
7651
if callable(cycle):
@@ -80,39 +55,25 @@ def examples(self) -> Sequence[object]:
8055
alt = [(base_values[i % len(base_values)]) for i in range(alt_length)]
8156
if alt:
8257
examples.append(list(alt))
83-
8458
return tuple(examples)
85-
86-
8759
@dataclass
8860
class TextStrategy(Strategy):
8961
alphabet: Strategy
9062
min_size: int
9163
max_size: int
92-
9364
def examples(self) -> Sequence[object]:
94-
chars = [str(c) for c in self.alphabet.examples() if str(c)]
95-
if not chars:
96-
chars = [""]
97-
65+
chars = [str(c) for c in self.alphabet.examples() if str(c)] or [""]
9866
examples: List[str] = [""]
9967
if self.min_size > 0:
100-
repeat_count = max(self.min_size, 1)
101-
repeat_count = min(repeat_count, self.max_size)
68+
repeat_count = min(max(self.min_size, 1), self.max_size)
10269
examples.append(chars[0] * repeat_count)
10370
if len(chars) > 1:
10471
joined = "".join(chars)
10572
examples.append(joined[: self.max_size])
10673
return tuple(examples)
107-
108-
10974
def sampled_from(values: Iterable[object]) -> SampledFromStrategy:
11075
return SampledFromStrategy(tuple(values))
111-
112-
11376
def lists(inner: Strategy, *, min_size: int, max_size: int) -> ListStrategy:
11477
return ListStrategy(inner=inner, min_size=min_size, max_size=max_size)
115-
116-
11778
def text(*, alphabet: Strategy, min_size: int, max_size: int) -> TextStrategy:
11879
return TextStrategy(alphabet=alphabet, min_size=min_size, max_size=max_size)

tests/conftest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55
import sys
66
from pathlib import Path
77

8+
if os.getenv("USE_HYPOTHESIS_STUB") == "1" or importlib.util.find_spec("hypothesis") is None:
9+
stub_path = Path(__file__).parent / "_stubs"
10+
stub_str = str(stub_path)
11+
if stub_str not in sys.path:
12+
sys.path.insert(0, stub_str)
13+
814
from _pytest.config import Config
915
from _pytest.config.argparsing import Parser
10-
from hypothesis import HealthCheck, Phase, settings
16+
from hypothesis import HealthCheck, Phase, settings # type: ignore
1117

1218
PROJECT_ROOT = Path(__file__).resolve().parents[1]
1319
SRC_PATH = PROJECT_ROOT / "src"

tests/test_canonical_invariance.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from sudoku_dlx import from_string, to_string, canonical_form
2+
3+
PUZ = (
4+
"53..7...."
5+
"6..195..."
6+
".98....6."
7+
"8...6...3"
8+
"4..8.3..1"
9+
"7...2...6"
10+
".6....28."
11+
"...419..5"
12+
"....8..79"
13+
)
14+
15+
def rot180(s: str) -> str:
16+
return s[::-1]
17+
18+
def swap_rows_in_band(grid, band, r1, r2):
19+
g = [row[:] for row in grid]
20+
base = band * 3
21+
g[base + r1], g[base + r2] = g[base + r2], g[base + r1]
22+
return g
23+
24+
def test_canonical_invariant_under_basic_isomorphs():
25+
g = from_string(PUZ)
26+
c0 = canonical_form(g)
27+
# rot180 string
28+
c1 = canonical_form(from_string(rot180(PUZ)))
29+
assert c1 == c0
30+
# swap two rows within a band
31+
g2 = swap_rows_in_band(g, band=0, r1=0, r2=2)
32+
c2 = canonical_form(g2)
33+
assert c2 == c0
34+
# relabel digits 1<->9 on the string
35+
trans = str.maketrans("123456789", "987654321")
36+
c3 = canonical_form(from_string(to_string(g).translate(trans)))
37+
assert c3 == c0

tests/test_formats_io.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from sudoku_dlx.formats import read_grids, write_grids, detect_format
2+
3+
PUZ = (
4+
"53..7...."
5+
"6..195..."
6+
".98....6."
7+
"8...6...3"
8+
"4..8.3..1"
9+
"7...2...6"
10+
".6....28."
11+
"...419..5"
12+
"....8..79"
13+
)
14+
15+
def test_txt_csv_jsonl_roundtrip(tmp_path):
16+
ptxt = tmp_path / "a.txt"
17+
ptxt.write_text(PUZ + "\n" + PUZ + "\n", encoding="utf-8")
18+
grids = read_grids(str(ptxt), "txt")
19+
assert grids and grids[0] == PUZ
20+
pcsv = tmp_path / "b.csv"
21+
write_grids(str(pcsv), grids, "csv")
22+
grids2 = read_grids(str(pcsv), "csv")
23+
assert grids2 == grids
24+
pjsonl = tmp_path / "c.jsonl"
25+
write_grids(str(pjsonl), grids2, "jsonl")
26+
grids3 = read_grids(str(pjsonl), "jsonl")
27+
assert grids3 == grids
28+
29+
def test_detect_format_defaults_txt(tmp_path):
30+
assert detect_format("x.sdk") == "txt"
31+
assert detect_format("x.ndjson") == "jsonl"
32+
assert detect_format("x.unknown") == "txt"

0 commit comments

Comments
 (0)