Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ The broad areas that need help:
compare by shape, dtype and contents with no tolerance, and datetimes and
UUIDs are quarantined rather than normalised (issue #12). Reopening either
means naming the real change the new policy would hide.
- **Performance.** Recording currently costs ~8x. `sys.monitoring`
(Python 3.12+) should be much cheaper than wrapping functions.
- **Performance.** Recording currently costs ~4x. Note that `sys.monitoring`
(Python 3.12+) is *not* the lever it looks like: profiling shows function
interception costs about 1% of the overhead, and serialisation the rest.
- **Portability.** Windows is supported and tested in CI. Where `SIGALRM`
does not exist the per-call timeout falls back to a watchdog thread, which
cannot interrupt a call blocked inside C code.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ otherwise, but the gap is real.
mode, size and a hash of the bytes). Network calls and database queries are
still invisible.

**Recording is slow.** Expect roughly an 8x slowdown on the recorded run.
**Recording is slow.** Expect roughly a 4x slowdown on the recorded run.
Tests that assert on wall-clock time may fail while recording.

## Measured behaviour
Expand Down Expand Up @@ -166,7 +166,7 @@ Start with the [good first issues][gfi]. The broad areas that need help:
- **Side-effect capture** — intercept `requests`, `sqlalchemy`
- **Comparators** for types that still need a policy — datetimes and UUIDs are
settled: quarantined, not normalised (issue #12)
- **Performance** — recording currently costs ~8x
- **Performance** — recording currently costs ~4x
- **Framework adapters** beyond pytest

The most valuable bug report of all is a **false positive**: if `nodrift check`
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "nodrift"
version = "0.1.3"
version = "0.1.4"
description = "Prove a refactor changed nothing — by replaying your tests' real inputs against both versions."
readme = "README.md"
requires-python = ">=3.9"
Expand Down
2 changes: 1 addition & 1 deletion src/nodrift/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
No model reviews the code. The verdict comes from execution.
"""

__version__ = "0.1.3"
__version__ = "0.1.4"

from .compare import compare
from .fingerprint import fingerprint
Expand Down
18 changes: 12 additions & 6 deletions src/nodrift/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ def cmd_check(args: argparse.Namespace) -> int:
return _print_report(report, args, abandoned)


def _plural(count: int, word: str) -> str:
""""1 function", "2 functions" — the report is read by people."""
return f"{count} {word}" if count == 1 else f"{count} {word}s"


def _print_not_covered(abandoned: list[str], verbose: bool) -> None:
"""Say which functions the verdict above does not speak for.

Expand All @@ -202,7 +207,7 @@ def _print_not_covered(abandoned: list[str], verbose: bool) -> None:
"""
if not abandoned:
return
print(f" {len(abandoned)} function(s) not fully recorded "
print(f" {_plural(len(abandoned), 'function')} not fully recorded "
f"(inputs too large, or not picklable) — not covered by this check")
if verbose:
for name in abandoned:
Expand All @@ -224,8 +229,8 @@ def _print_report(report: dict, args: argparse.Namespace,
quarantined = report.get("quarantined", 0)

if quarantined:
print(f" {quarantined} record(s) quarantined as nondeterministic "
f"(not compared)")
print(f" {_plural(quarantined, 'record')} quarantined as "
f"nondeterministic (not compared)")
# A bare count tells a user nothing about whether the function they
# care about is among the ones that stopped being checked.
targets = report.get("quarantined_targets") or []
Expand All @@ -241,12 +246,13 @@ def _print_report(report: dict, args: argparse.Namespace,
_print_not_covered(abandoned, getattr(args, "verbose", False))

if not differs:
print(f"\n no behaviour change across {total} recorded inputs "
f"in {report['total_targets']} functions\n")
print(f"\n no behaviour change across "
f"{_plural(total, 'recorded input')} in "
f"{_plural(report['total_targets'], 'function')}\n")
return 0

print(f"\n {differs} of {total} recorded inputs behave differently "
f"({report['changed_targets']} functions)\n")
f"({_plural(report['changed_targets'], 'function')})\n")
for target, counts in sorted(
report["changed"].items(), key=lambda kv: -kv[1]["differs"]
):
Expand Down
Loading