Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- iter_deltas: validate each candidate mutation with ast.parse before yielding it; invalid mutations (e.g. DefinitionDrop leaving an empty class body) are now silently skipped and logged at TRACE level - Rename the local `ast` variable to `tree` to avoid shadowing the newly-imported ast module - install_module_loader: replace deprecated imp.new_module() (removed in Python 3.12) with types.ModuleType() - Add regression tests: test_no_syntax_error_mutations_empty_class_body and test_no_syntax_error_mutations_docstring - Mark the two resolved TODOs in README.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sqlite3 is in the stdlib and works everywhere including PyPy. lsm-db and cython were the last hard CPython-only C-extension dependencies. - Add Database wrapper class with the same slice/item interface as LSM - WAL mode + per-call timeout (defaulting to 300s, scaled from alpha in mutation_pass) for safe concurrent thread-pool writes - Rename storage file .mutation.okvslite → .mutation.db - Remove lsm-db and cython from requirements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the last non-stdlib runtime dependency (lexode) by replacing the generic key-value Database class with three typed tables (config, mutations, results) and purpose-built methods. Also fixes a latent bug in mutation_apply where a UUID object was passed instead of its bytes representation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ptions
- Change --include/--exclude from comma-separated to repeatable flags
(docopt [--include=<glob>]... syntax collects values into a list)
- Remove .split(",") in play_create_mutations; defaults are now lists
- Expand Options docstring with descriptions for all flags
- Update README: fix "No database" claim, add Options section documenting
--include/--exclude, --sampling, --randomly-seed, and -- TEST-COMMAND
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… projects Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…test/SKILL.md) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add conftest.py to register mutation.py as a pytest plugin (makes --mutation flag recognized when running foobar/tests.py) - Add foobar/__init__.py so foobar is a package and install_module_loader patches the correct sys.modules key (foobar.ex) - Fix foobar/tests.py: use 'from foobar import ex', assert return value, add test_001 to kill the a^2 equivalent-for-42 mutation - Add 'make check-foobar' target; exits 0 even with survivors (8 remaining are equivalent mutations on dead code and docstrings) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace humanize, termcolor, zstandard, ulid, aiostream, pygments, tqdm, and loguru with stdlib equivalents (zlib, logging, asyncio, etc). Remove conftest.py that caused double-registration of --mutation flag. Only coverage, docopt, and pytest remain as external dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add make check, check-with-coverage, check-fail-fast, check-survivors, lock
- add .github/workflows/ci.yml running the three make check-* targets
- remove requirements.{txt,dev.txt,source.txt} (superseded by uv.lock)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ard, ulid, aiostream, pygments, tqdm, loguru) Replace each with stdlib equivalents or inline code: - humanize → inline humanize() using integer arithmetic - termcolor → green()/red() ANSI helpers - zstandard → zlib (compress/decompress) - ulid → make_uid() with ms-precision timestamp + random bytes - aiostream → plain asyncio.wait() loop - pygments → line-by-line ANSI diff printer - tqdm → inline Progress class - loguru → _Logger shim backed by stdlib logging Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parses a list of strings into (keywords, standalone, extra):
- '--key=val' → ('--key', 'val'); bare flags → ('--key', True)
- positional args go into standalone
- everything after '--' goes into extra
10 tests covering the reference cases (~check-cli-00/01) plus
edge cases: empty input, bare separator, value-with-equals, order.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the last external CLI-parsing dependency by wiring the in-house cli_read() parser into main(). Dispatches subcommands via structural pattern matching. Renames PYTEST-COMMAND → PYTEST_EXTRA to align with cli_read's extra return value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
check_tests() is called by both play and replay; replay passes a stored command directly (command is not None), so arguments never has <file-or-directory> or PYTEST_EXTRA keys, causing a KeyError. The check only applies to the play path, so it now lives in main(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
loguru does not support format specs like {:>6,} in its placeholder
syntax — the values were never interpolated. Use str.format() to
build the string first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
patch() does not validate context lines — it silently applies diffs at the given line numbers using the diff's own text, not the source. This meant gc kept stale .mutations.ignored/ files even when the source had changed and the mutation no longer applied. Add _diff_applies() which verifies context and remove lines against the current source before trusting a diff is still valid. Also fix test_mutation_ignored_gc_keeps_valid to use the production diff() function so the diff format matches what the tool actually stores. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…T_EXTRA→PYTEST_BASE_COMMAND - Rename mutation_pass to mutation_is_survivor; return (uid, bool) instead of writing DB directly - Rename play/play_mutations to mutation_play/mutation_exec - Rename PYTEST_EXTRA argument key to PYTEST_BASE_COMMAND throughout - Move DB writes to callers; open connections more locally in replay_mutation, mutation_diff_size, etc. - Progress: add delta param to throttle print frequency (every N updates) - make check-foobar: clean .mutation.db and .mutations.ignored before run; add foobar/tests.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…port The new patch() validation checked context/remove lines against source, but diffs are generated from ast.unparse (canonical) and then applied to the same canonical form — not necessarily the original source string. The check was too strict and broke the existing tests. Stale-diff detection is handled separately by _diff_applies(), which is called only from mutation_ignored_gc(), so patch() itself doesn't need to validate context lines. Also removes the now-unused functools import (ruff F401). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
patch() no longer validates context lines (removed in previous commit), so stale diffs were silently producing garbage patched modules instead of exiting with EXIT_STALE. The subprocess then ran tests against the mangled module and reported a normal pass/fail, bypassing the stale classification path entirely. Fix: call _diff_applies() on the canonical source before patching. If the diff doesn't apply, raise an exception which pytest_configure catches and converts to sys.exit(EXIT_STALE), triggering the stale classification in mutation_is_survivor / replay_mutation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Module, class, and function docstrings are string constants whose parent is an ast.Expr at position 0 in a body. Mutating them produces noise (the test suite rarely checks docstring content) and generates false positives. Added _is_docstring() helper and a guard at the top of MutateString.mutate(). Added test confirming only non-docstring strings are mutated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, staleness was only detected inside the pytest subprocess (install_module_loader → EXIT_STALE). Now replay_mutation checks _diff_applies() directly before running pytest, giving immediate feedback without the subprocess overhead. Also suppress "No mutation failures 👍" when the queue empties solely because stale mutations were classified — that message should only appear when the user actively fixed mutations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
max_workers was immediately set to 1 then checked with > 1, so --numprocesses was never passed. Remove the dead code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
patch() operates on ast.unparse output; applying deltas to the original source could silently pass on broken diffs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chunks() iterated elements of a single slice instead of yielding chunks; it was never called anywhere. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover subscript KeyError/IndexError injection, ZeroDivisionError on division, ValueError on int() calls, and the guarded-skip behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover replacement with pass, skipping of pass/expr nodes, multi-definition drop, and sole-definition guard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mutation_is_survivor previously opened the database and decompressed the diff for every mutation just to check the ignored-file hash. Now the set of known hashes is built once from .mutations.ignored/ filenames and passed in the args tuple; the DB read is skipped entirely when no mutations are ignored (the common case). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mutation_all and mutation_without_inject_exception were named module-level functions used in exactly one place. Replace with inline lambdas. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old assertion split on 'def' to skip the function signature, which would break if the function name contained 'not'. Check for 'return x' directly instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… pickled) Lambdas defined inside play_create_mutations are not picklable and crash the ProcessPoolExecutor. Restore mutation_all and mutation_without_inject_exception as module-level functions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Format mutation.py, tests.py, foobar/ with ruff (line length, spacing) - Fix unused variables: noqa F841 in foobar/ex.py, remove dead canonical= assignments in tests.py - Fix unclosed Database warnings: wrap _make_db tests in `with db:` - Fix makefile: --cov=mutations.py → --cov=mutation - Add ruff lint step to CI so linting runs on every push/PR Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…utate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rmat - Restore MutateExceptionHandler class definition accidentally removed by b1e3745 (which left predicate/mutate orphaned at module scope) - Fix test_mutation_ignored_gc_keeps_valid to use lineterm="" and split("\n") matching the format produced by mutation.py's diff(), so patch() can apply it correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…it is the error) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes:
devvsorigin/mainSummary
The
devbranch is a significant refactor ofmutation.pyfocused on eliminating external dependencies, improving correctness, expanding test coverage, and improving CLI ergonomics. Version bumped from0.5.0→0.6.0.Dependencies removed
Both
aiostreamanddocopthave been dropped.mutation.pynow depends only oncoverage(plus stdlib). All removed functionality was replaced inline:docoptcli_read()— custom argument parseraiostreamasyncio.wait()looppygmentstqdmProgressclass (inline)loguru_Loggershim wrapping stdliblogginghumanizehumanize()function (inline)termcolorgreen()/red()ANSI helperszstandardzlibulidmake_uid()— ms timestamp + random bytesNew features
cli_read()— custom argument parserReplaces
docopt. Parsessys.argvinto(keywords, standalone, extra)tuples. The--sentinel separates pytest passthrough args from mutation args. Fully tested with 9 new unit tests._diff_applies()— stale mutation detectionNew function that validates whether a stored diff still applies to the current source before patching. Used in:
mutation_ignored_gc— prunes stale ignored mutationsinstall_module_loader— exits withEXIT_STALE = 5if the mutation is staleCLASSIFICATION_STALE = 6/EXIT_STALE = 5New classification for mutations whose diffs no longer apply to current source. Stale mutations are excluded from the replay queue.
MutateStringskips docstrings_is_docstring(node, tree)helper added. String mutations are no longer applied to module/class/function docstrings, reducing noise.Fixes and correctness
make_uid(): Fixed overflow — now uses milliseconds (time.time_ns() // 1_000_000) as 6 bytes instead of microseconds as 8 bytes.mutation_pass→mutation_is_survivor: Renamed for clarity.replay_mutation/mutation_diff_size:dbargument dropped — functions now open their own DB connection.run():timeoutis now a required argument (no defaultNone) to prevent accidental unbounded waits.patch(): Variablelrenamed toln(ruff E741 fix). Context-line validation intentionally not enforced (diffs are againstast.unparsecanonical form, not original source).ForceConditional: Now coverswhile,assert, and ternary (IfExp) nodes, not justif.MutateIterator: Now handlesAsyncForin addition toFor.MutateExceptionHandler: Class header was missing (orphaned methods at module scope) — restored.Progress.update(): Addeddeltaparameter to reduce terminal noise on large runs.functools,itertools,from ast import Constant,from uuid import UUID, and duplicate imports.Test coverage added
24 new test functions covering:
ForceConditional—while,assert, ternaryMutateIterator—AsyncForMutateString— docstring skippingStatementDrop— basic cases, skipspass/bare-exprDefinitionDrop— basic cases, sole-definition guardInjectException— subscript (string key, int key), division, guarded skip,int()callcli_read— 9 tests covering all edge casesMakefile
checknow runspytest tests.py+ruff check(was: running mutation play on foobar)check-with-coveragenow coversmutationmodule (was:foobar)check-survivorscommand fixed: uses--exclude="foobar/test*.py"to avoid treating test files as sourcelint,doc,clean,todo,xxx,serve,wipCI (
.github/workflows/ci.yml)origin/main)uv sync→ruff check→check-fail-fast→check-with-coverage→check-survivorscheck-survivorsruns withcontinue-on-error: true— survivors are expected and should not block CIfoobar/example projectfoobar/ex.py: Added a dead-code branch (untestedno_opfunction) to demonstrateDefinitionDropfoobar/tests.py: Added one additional assertion