Skip to content
Open
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
7 changes: 4 additions & 3 deletions docker/Dockerfile.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ENV UV_PROJECT_ENVIRONMENT=/opt/venv

COPY . .

RUN uv sync --group dev
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project

ENTRYPOINT ["uv", "run", "pytest"]
CMD ["--verbose"]
1 change: 1 addition & 0 deletions e2e_projects/type_checking_mypy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This project uses mypy for type checking to detect invalid mutants.
25 changes: 25 additions & 0 deletions e2e_projects/type_checking_mypy/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[project]
name = "type-checking-mypy"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = []
requires-python = ">=3.10"
dependencies = []

[build-system]
requires = ["uv_build>=0.9.18,<0.10.0"]
build-backend = "uv_build"

[dependency-groups]
dev = [
"mypy>=1.0.0",
"pytest>=8.2.0",
]

[tool.mutmut]
debug = true
type_check_command = ["mypy", "--output", "json", "src"]

[tool.mypy]
strict = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def hello() -> str:
greeting = "Hello from type-checking-mypy!"
return greeting

def mutate_me() -> str:
# verify that hello() keeps the return type str
# (if not, this will type error and not be mutated)
return hello() + " Goodbye"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from type_checking_mypy import mutate_me

def test_mutate_me() -> None:
assert mutate_me() == "Hello from type-checking-mypy! Goodbye"
309 changes: 309 additions & 0 deletions e2e_projects/type_checking_mypy/uv.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ source-include = ["HISTORY.rst"]
[dependency-groups]
dev = [
"inline-snapshot>=0.32.0",
"mypy>=1.0.0",
"pre-commit>=4.5.1",
"pyrefly>=0.53.0",
"pytest-asyncio>=1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/mutmut/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1778,7 +1778,7 @@ def get_mutant_name_from_selection(self) -> str | None:
# noinspection PyTypeChecker
mutants_table: DataTable[Any] = self.query_one("#mutants") # type: ignore[assignment]
if mutants_table.cursor_row is None or not mutants_table.is_valid_row_index(mutants_table.cursor_row):
return
return None

return str(mutants_table.get_row_at(mutants_table.cursor_row)[0])

Expand Down
46 changes: 29 additions & 17 deletions src/mutmut/file_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,26 +323,38 @@ def _get_local_name(func_name: str) -> cst.BaseExpression:
cst.Arg(cst.Name("None" if class_name is None else "self")),
],
)
# for non-async functions, simply return the value or generator
result_statement = cst.SimpleStatementLine([cst.Return(result)])

if function.asynchronous:
is_generator = _is_generator(function)
if is_generator:
# async for i in _mutmut_trampoline(...): yield i
result_statement = cst.For( # type: ignore[assignment]
target=cst.Name("i"),
iter=result,
body=cst.IndentedBlock([cst.SimpleStatementLine([cst.Expr(cst.Yield(cst.Name("i")))])]),
asynchronous=cst.Asynchronous(),
)
else:
# return await _mutmut_trampoline(...)
result_statement = cst.SimpleStatementLine([cst.Return(cst.Await(result))])

type_ignore_whitespace = cst.TrailingWhitespace(comment=cst.Comment("# type: ignore"))

function.whitespace_after_type_parameters
result_statement: cst.SimpleStatementLine | cst.For
if not function.asynchronous:
# for non-async functions, simply return the value or generator
result_statement = cst.SimpleStatementLine(
[cst.Return(result)],
trailing_whitespace=type_ignore_whitespace,
)
elif not _is_generator(function):
# return await _mutmut_trampoline(...)
result_statement = cst.SimpleStatementLine(
[cst.Return(cst.Await(result))],
trailing_whitespace=type_ignore_whitespace,
)
else:
# async for i in _mutmut_trampoline(...): yield i
result_statement = cst.For(
target=cst.Name("i"),
iter=result,
body=cst.IndentedBlock(
[
cst.SimpleStatementLine(
[cst.Expr(cst.Yield(cst.Name("i")))],
trailing_whitespace=type_ignore_whitespace,
)
]
),
asynchronous=cst.Asynchronous(),
)

return function.with_changes(
body=cst.IndentedBlock(
[
Expand Down
36 changes: 18 additions & 18 deletions src/mutmut/trampoline_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,27 @@ def mangle_function_name(*, name: str, class_name: str | None) -> str:

def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None): # type: ignore
\"""Forward call to original or mutated function, depending on the environment\"""
import os # type: ignore
mutant_under_test = os.environ['MUTANT_UNDER_TEST'] # type: ignore
if mutant_under_test == 'fail': # type: ignore
import os
mutant_under_test = os.environ['MUTANT_UNDER_TEST']
if mutant_under_test == 'fail':
from mutmut.__main__ import MutmutProgrammaticFailException # type: ignore
raise MutmutProgrammaticFailException('Failed programmatically') # type: ignore
elif mutant_under_test == 'stats': # type: ignore
from mutmut.__main__ import record_trampoline_hit # type: ignore
record_trampoline_hit(orig.__module__ + '.' + orig.__name__) # type: ignore
raise MutmutProgrammaticFailException('Failed programmatically')
elif mutant_under_test == 'stats':
from mutmut.__main__ import record_trampoline_hit
record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
# (for class methods, orig is bound and thus does not need the explicit self argument)
result = orig(*call_args, **call_kwargs) # type: ignore
return result # type: ignore
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_' # type: ignore
if not mutant_under_test.startswith(prefix): # type: ignore
result = orig(*call_args, **call_kwargs) # type: ignore
return result # type: ignore
mutant_name = mutant_under_test.rpartition('.')[-1] # type: ignore
if self_arg is not None: # type: ignore
result = orig(*call_args, **call_kwargs)
return result
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
if not mutant_under_test.startswith(prefix):
result = orig(*call_args, **call_kwargs)
return result
mutant_name = mutant_under_test.rpartition('.')[-1]
if self_arg is not None:
# call to a class method where self is not bound
result = mutants[mutant_name](self_arg, *call_args, **call_kwargs) # type: ignore
result = mutants[mutant_name](self_arg, *call_args, **call_kwargs)
else:
result = mutants[mutant_name](*call_args, **call_kwargs) # type: ignore
return result # type: ignore
result = mutants[mutant_name](*call_args, **call_kwargs)
return result

"""
20 changes: 20 additions & 0 deletions tests/e2e/test_e2e_type_checking_mypy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from inline_snapshot import snapshot

from tests.e2e.e2e_utils import run_mutmut_on_project


def test_type_checking_mypy_result_snapshot():
assert run_mutmut_on_project("type_checking_mypy") == snapshot(
{
"mutants/src/type_checking_mypy/__init__.py.meta": {
"type_checking_mypy.x_hello__mutmut_1": 37,
"type_checking_mypy.x_hello__mutmut_2": 1,
"type_checking_mypy.x_hello__mutmut_3": 1,
"type_checking_mypy.x_hello__mutmut_4": 1,
"type_checking_mypy.x_mutate_me__mutmut_1": 37,
"type_checking_mypy.x_mutate_me__mutmut_2": 1,
"type_checking_mypy.x_mutate_me__mutmut_3": 1,
"type_checking_mypy.x_mutate_me__mutmut_4": 1,
}
}
)
52 changes: 26 additions & 26 deletions tests/test_mutation regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_create_trampoline_wrapper_async_method():
async def foo(a: str, b, *args, **kwargs) -> dict[str, int]:
args = [a, b, *args]# type: ignore
kwargs = {**kwargs}# type: ignore
return await _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)\
return await _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)# type: ignore\
""")


Expand All @@ -34,7 +34,7 @@ async def foo():
args = []# type: ignore
kwargs = {}# type: ignore
async for i in _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None):
yield i\
yield i# type: ignore\
""")


Expand All @@ -45,7 +45,7 @@ def test_create_trampoline_wrapper_with_positionals_only_args():
def foo(p1, p2=None, /, p_or_kw=None, *, kw):
args = [p1, p2, p_or_kw]# type: ignore
kwargs = {'kw': kw}# type: ignore
return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)\
return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)# type: ignore\
""")


Expand All @@ -56,7 +56,7 @@ def test_create_trampoline_wrapper_for_class_method():
def foo(self, a, b):
args = [a, b]# type: ignore
kwargs = {}# type: ignore
return _mutmut_trampoline(object.__getattribute__(self, 'x_foo__mutmut_orig'), object.__getattribute__(self, 'x_foo__mutmut_mutants'), args, kwargs, self)\
return _mutmut_trampoline(object.__getattribute__(self, 'x_foo__mutmut_orig'), object.__getattribute__(self, 'x_foo__mutmut_mutants'), args, kwargs, self)# type: ignore\
""")


Expand Down Expand Up @@ -99,33 +99,33 @@ def add(self, value):

def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None): # type: ignore
"""Forward call to original or mutated function, depending on the environment"""
import os # type: ignore
mutant_under_test = os.environ['MUTANT_UNDER_TEST'] # type: ignore
if mutant_under_test == 'fail': # type: ignore
import os
mutant_under_test = os.environ['MUTANT_UNDER_TEST']
if mutant_under_test == 'fail':
from mutmut.__main__ import MutmutProgrammaticFailException # type: ignore
raise MutmutProgrammaticFailException('Failed programmatically') # type: ignore
elif mutant_under_test == 'stats': # type: ignore
from mutmut.__main__ import record_trampoline_hit # type: ignore
record_trampoline_hit(orig.__module__ + '.' + orig.__name__) # type: ignore
raise MutmutProgrammaticFailException('Failed programmatically')
elif mutant_under_test == 'stats':
from mutmut.__main__ import record_trampoline_hit
record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
# (for class methods, orig is bound and thus does not need the explicit self argument)
result = orig(*call_args, **call_kwargs) # type: ignore
return result # type: ignore
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_' # type: ignore
if not mutant_under_test.startswith(prefix): # type: ignore
result = orig(*call_args, **call_kwargs) # type: ignore
return result # type: ignore
mutant_name = mutant_under_test.rpartition('.')[-1] # type: ignore
if self_arg is not None: # type: ignore
result = orig(*call_args, **call_kwargs)
return result
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
if not mutant_under_test.startswith(prefix):
result = orig(*call_args, **call_kwargs)
return result
mutant_name = mutant_under_test.rpartition('.')[-1]
if self_arg is not None:
# call to a class method where self is not bound
result = mutants[mutant_name](self_arg, *call_args, **call_kwargs) # type: ignore
result = mutants[mutant_name](self_arg, *call_args, **call_kwargs)
else:
result = mutants[mutant_name](*call_args, **call_kwargs) # type: ignore
return result # type: ignore
result = mutants[mutant_name](*call_args, **call_kwargs)
return result

def foo(a: list[int], b):
args = [a, b]# type: ignore
kwargs = {}# type: ignore
return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)
return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)# type: ignore

def x_foo__mutmut_orig(a: list[int], b):
return a[0] > b
Expand All @@ -145,7 +145,7 @@ def x_foo__mutmut_2(a: list[int], b):
def bar():
args = []# type: ignore
kwargs = {}# type: ignore
return _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs, None)
return _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs, None)# type: ignore

def x_bar__mutmut_orig():
yield 1
Expand All @@ -162,7 +162,7 @@ class Adder:
def __init__(self, amount):
args = [amount]# type: ignore
kwargs = {}# type: ignore
return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_mutants'), args, kwargs, self)
return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_mutants'), args, kwargs, self)# type: ignore
def xǁAdderǁ__init____mutmut_orig(self, amount):
self.amount = amount
def xǁAdderǁ__init____mutmut_1(self, amount):
Expand All @@ -176,7 +176,7 @@ def xǁAdderǁ__init____mutmut_1(self, amount):
def add(self, value):
args = [value]# type: ignore
kwargs = {}# type: ignore
return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁadd__mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁadd__mutmut_mutants'), args, kwargs, self)
return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁadd__mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁadd__mutmut_mutants'), args, kwargs, self)# type: ignore

def xǁAdderǁadd__mutmut_orig(self, value):
return self.amount + value
Expand Down
Loading
Loading