Skip to content

Commit 00778dc

Browse files
committed
WIP pyrefly
1 parent 77928e8 commit 00778dc

File tree

7 files changed

+42
-45
lines changed

7 files changed

+42
-45
lines changed

e2e_projects/type_checking/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ dev = [
2121
[tool.mutmut]
2222
debug = true
2323
# TODO: pyrefly ignores mutants/ dir if it is in .gitignore
24-
# type_check_command = ["pyrefly", "check", "--output-format=json", "--use-ignore-files=false"]
25-
type_check_command = ["pyright", "--outputjson"]
24+
type_check_command = ["pyrefly", "check", "--output-format=json", "--use-ignore-files=false"]
25+
# type_check_command = ["pyright", "--outputjson"]
2626

2727
[tool.pyrefly]
2828
project-includes = [

src/mutmut/__main__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
'timeout': '⏰',
9898
'suspicious': '🤔',
9999
'skipped': '🔇',
100-
'caught by type check': 'ö',
100+
'caught by type check': '🧙',
101101
'check was interrupted by user': '🛑',
102102
'not checked': '?',
103103
'killed': '🎉',
@@ -319,6 +319,7 @@ def __init__(self, *, path):
319319
self.key_by_pid = {}
320320
self.exit_code_by_key = {}
321321
self.durations_by_key = {}
322+
self.type_check_error_by_key = {}
322323
self.hash_by_function_name = {}
323324
self.start_time_by_pid = {}
324325

@@ -333,7 +334,8 @@ def load(self):
333334
self.hash_by_function_name = meta.pop('hash_by_function_name')
334335
self.durations_by_key = meta.pop('durations_by_key')
335336
self.estimated_time_of_tests_by_mutant = meta.pop('estimated_durations_by_key')
336-
assert not meta, f'Meta file {self.meta_path} constains unexpected keys: {set(meta.keys())}'
337+
self.type_check_error_by_key = meta.pop('type_check_error_by_key')
338+
assert not meta, f'Meta file {self.meta_path} contains unexpected keys: {set(meta.keys())}'
337339

338340
def register_pid(self, *, pid, key):
339341
self.key_by_pid[pid] = key
@@ -361,6 +363,7 @@ def save(self):
361363
exit_code_by_key=self.exit_code_by_key,
362364
hash_by_function_name=self.hash_by_function_name,
363365
durations_by_key=self.durations_by_key,
366+
type_check_error_by_key=self.type_check_error_by_key,
364367
estimated_durations_by_key=self.estimated_time_of_tests_by_mutant,
365368
), f, indent=4)
366369

@@ -402,6 +405,7 @@ def filter_mutants_with_type_checker():
402405
mutants_to_skip.append(FailedTypeCheckMutant(
403406
method_location=mutant,
404407
name=mutant_name,
408+
error=error,
405409
))
406410

407411
return mutants_to_skip
@@ -427,6 +431,7 @@ class MutatedMethodLocation:
427431
class FailedTypeCheckMutant:
428432
method_location: MutatedMethodLocation
429433
name: str
434+
error: TypeCheckingError
430435

431436

432437
class MutatedMethodsCollector(cst.CSTVisitor):
@@ -750,7 +755,7 @@ def calculate_summary_stats(source_file_mutation_data_by_path):
750755

751756
def print_stats(source_file_mutation_data_by_path, force_output=False):
752757
s = calculate_summary_stats(source_file_mutation_data_by_path)
753-
print_status(f'{(s.total - s.not_checked)}/{s.total} 🎉 {s.killed} 🫥 {s.no_tests}{s.timeout} 🤔 {s.suspicious} 🙁 {s.survived} 🔇 {s.skipped} X {s.caught_by_type_check}', force_output=force_output)
758+
print_status(f'{(s.total - s.not_checked)}/{s.total} 🎉 {s.killed} 🫥 {s.no_tests}{s.timeout} 🤔 {s.suspicious} 🙁 {s.survived} 🔇 {s.skipped} 🧙 {s.caught_by_type_check}', force_output=force_output)
754759

755760

756761
def run_forced_fail_test(runner):
@@ -1264,6 +1269,7 @@ def read_one_child_exit_status():
12641269
failed_type_check_mutant = next((m for m in failed_type_check_mutants if m.name == mutant_name), None)
12651270
if failed_type_check_mutant:
12661271
m.exit_code_by_key[mutant_name] = 6
1272+
m.type_check_error_by_key[mutant_name] = failed_type_check_mutant.error.error_description
12671273
m.save()
12681274
continue
12691275

@@ -1598,6 +1604,7 @@ def on_data_table_row_highlighted(self, event):
15981604
status = status_by_exit_code[exit_code]
15991605
estimated_duration = source_file_mutation_data.estimated_time_of_tests_by_mutant.get(mutant_name, '?')
16001606
duration = source_file_mutation_data.durations_by_key.get(mutant_name, '?')
1607+
type_check_error = source_file_mutation_data.type_check_error_by_key.get(mutant_name, '?')
16011608

16021609
view_tests_description = f'(press t to view tests executed for this mutant)'
16031610

@@ -1610,6 +1617,8 @@ def on_data_table_row_highlighted(self, event):
16101617
description = f'Skipped ({exit_code=})'
16111618
case 'check was interrupted by user':
16121619
description = f'User interrupted ({exit_code=})'
1620+
case 'caught by type check':
1621+
description = f'Caught by type checker ({exit_code=}): {type_check_error}'
16131622
case 'timeout':
16141623
description = (f'Timeout ({exit_code=}): Timed out because tests did not finish within {duration:.3f} seconds. '
16151624
f'Tests without mutation took {estimated_duration:.3f} seconds. {view_tests_description}')

src/mutmut/file_mutation.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from mutmut.trampoline_templates import create_trampoline_lookup, mangle_function_name, trampoline_impl
1111
from mutmut.node_mutation import mutation_operators, OPERATORS_TYPE
1212

13-
NEVER_MUTATE_FUNCTION_NAMES = { "__getattribute__", "__setattr__", "__new__", "__init__" }
13+
NEVER_MUTATE_FUNCTION_NAMES = { "__getattribute__", "__setattr__", "__new__"}
1414
NEVER_MUTATE_FUNCTION_CALLS = { "len", "isinstance" }
1515

1616
@dataclass
@@ -158,10 +158,6 @@ def _skip_node_and_children(self, node: cst.CSTNode):
158158
if isinstance(node, (cst.FunctionDef, cst.ClassDef)) and len(node.decorators):
159159
return True
160160

161-
if isinstance(node, cst.ClassDef):
162-
# TODO: do not skip classes for type checking
163-
return True
164-
165161
return False
166162

167163

tests/e2e/test_e2e_my_lib.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import pytest
21
from inline_snapshot import snapshot
32

43
from tests.e2e.e2e_utils import run_mutmut_on_project
54

65

7-
@pytest.mark.skip(reason="WIP type checking implementation")
86
def test_my_lib_result_snapshot():
97
assert run_mutmut_on_project("my_lib") == snapshot(
108
{

tests/e2e/test_e2e_type_checking.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def test_type_checking_result_snapshot():
1313
"type_checking.x_hello__mutmut_4": 1,
1414
"type_checking.x_a_hello_wrapper__mutmut_1": 6,
1515
"type_checking.x_a_hello_wrapper__mutmut_2": 0,
16+
"type_checking.xǁPersonǁset_name__mutmut_1": 6,
1617
"type_checking.x_mutate_me__mutmut_1": 6,
1718
"type_checking.x_mutate_me__mutmut_2": 6,
1819
"type_checking.x_mutate_me__mutmut_3": 1,

tests/test_mutation regression.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import pytest
21
from inline_snapshot import snapshot
32
import libcst as cst
43

@@ -66,7 +65,6 @@ def foo(self, a, b):
6665
""")
6766

6867

69-
@pytest.mark.skip(reason="WIP type checking implementation")
7068
def test_module_mutation():
7169
"""Regression test, for a complete module with functions, type annotations and a class"""
7270

@@ -101,33 +99,33 @@ def add(self, value):
10199
from typing import Callable
102100
from typing import ClassVar
103101
104-
MutantDict = Annotated[dict[str, Callable], "Mutant"]
102+
MutantDict = Annotated[dict[str, Callable], "Mutant"] # type: ignore
105103
106104
107-
def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None):
105+
def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None): # type: ignore
108106
"""Forward call to original or mutated function, depending on the environment"""
109-
import os
110-
mutant_under_test = os.environ['MUTANT_UNDER_TEST']
111-
if mutant_under_test == 'fail':
112-
from mutmut.__main__ import MutmutProgrammaticFailException
113-
raise MutmutProgrammaticFailException('Failed programmatically') \n\
114-
elif mutant_under_test == 'stats':
115-
from mutmut.__main__ import record_trampoline_hit
116-
record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
107+
import os # type: ignore
108+
mutant_under_test = os.environ['MUTANT_UNDER_TEST'] # type: ignore
109+
if mutant_under_test == 'fail': # type: ignore
110+
from mutmut.__main__ import MutmutProgrammaticFailException # type: ignore
111+
raise MutmutProgrammaticFailException('Failed programmatically') # type: ignore
112+
elif mutant_under_test == 'stats': # type: ignore
113+
from mutmut.__main__ import record_trampoline_hit # type: ignore
114+
record_trampoline_hit(orig.__module__ + '.' + orig.__name__) # type: ignore
117115
# (for class methods, orig is bound and thus does not need the explicit self argument)
118-
result = orig(*call_args, **call_kwargs)
119-
return result
120-
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
121-
if not mutant_under_test.startswith(prefix):
122-
result = orig(*call_args, **call_kwargs)
123-
return result
124-
mutant_name = mutant_under_test.rpartition('.')[-1]
125-
if self_arg is not None:
116+
result = orig(*call_args, **call_kwargs) # type: ignore
117+
return result # type: ignore
118+
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_' # type: ignore
119+
if not mutant_under_test.startswith(prefix): # type: ignore
120+
result = orig(*call_args, **call_kwargs) # type: ignore
121+
return result # type: ignore
122+
mutant_name = mutant_under_test.rpartition('.')[-1] # type: ignore
123+
if self_arg is not None: # type: ignore
126124
# call to a class method where self is not bound
127-
result = mutants[mutant_name](self_arg, *call_args, **call_kwargs)
125+
result = mutants[mutant_name](self_arg, *call_args, **call_kwargs) # type: ignore
128126
else:
129-
result = mutants[mutant_name](*call_args, **call_kwargs)
130-
return result
127+
result = mutants[mutant_name](*call_args, **call_kwargs) # type: ignore
128+
return result # type: ignore
131129
132130
def x_foo__mutmut_orig(a: list[int], b):
133131
return a[0] > b
@@ -143,7 +141,7 @@ def foo(a: list[int], b):
143141
kwargs = {}
144142
return _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)
145143
146-
x_foo__mutmut_mutants : ClassVar[MutantDict] = {
144+
x_foo__mutmut_mutants : ClassVar[MutantDict] = { # type: ignore
147145
'x_foo__mutmut_1': x_foo__mutmut_1, \n\
148146
'x_foo__mutmut_2': x_foo__mutmut_2
149147
}
@@ -160,7 +158,7 @@ def bar():
160158
kwargs = {}
161159
return _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs, None)
162160
163-
x_bar__mutmut_mutants : ClassVar[MutantDict] = {
161+
x_bar__mutmut_mutants : ClassVar[MutantDict] = { # type: ignore
164162
'x_bar__mutmut_1': x_bar__mutmut_1
165163
}
166164
x_bar__mutmut_orig.__name__ = 'x_bar'
@@ -175,7 +173,7 @@ def __init__(self, amount):
175173
kwargs = {}
176174
return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁ__init____mutmut_mutants'), args, kwargs, self)
177175
\n\
178-
xǁAdderǁ__init____mutmut_mutants : ClassVar[MutantDict] = {
176+
xǁAdderǁ__init____mutmut_mutants : ClassVar[MutantDict] = { # type: ignore
179177
'xǁAdderǁ__init____mutmut_1': xǁAdderǁ__init____mutmut_1
180178
}
181179
xǁAdderǁ__init____mutmut_orig.__name__ = 'xǁAdderǁ__init__'
@@ -191,7 +189,7 @@ def add(self, value):
191189
kwargs = {}
192190
return _mutmut_trampoline(object.__getattribute__(self, 'xǁAdderǁadd__mutmut_orig'), object.__getattribute__(self, 'xǁAdderǁadd__mutmut_mutants'), args, kwargs, self)
193191
\n\
194-
xǁAdderǁadd__mutmut_mutants : ClassVar[MutantDict] = {
192+
xǁAdderǁadd__mutmut_mutants : ClassVar[MutantDict] = { # type: ignore
195193
'xǁAdderǁadd__mutmut_1': xǁAdderǁadd__mutmut_1
196194
}
197195
xǁAdderǁadd__mutmut_orig.__name__ = 'xǁAdderǁadd'

tests/test_mutation.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
from unittest.mock import Mock, patch
55

66
import libcst as cst
7-
import pytest
8-
from inline_snapshot import snapshot
97

108
import mutmut
119
from mutmut.__main__ import (
@@ -303,7 +301,6 @@ def concat():
303301
assert sorted(mutants) == []
304302

305303

306-
@pytest.mark.skip(reason="WIP type checking implementation")
307304
def test_basic_class():
308305
source = """
309306
class Foo:
@@ -493,7 +490,6 @@ def test_mangle_function_name():
493490
assert mangle_function_name(name='bar', class_name='Foo') == f'x{CLASS_NAME_SEPARATOR}Foo{CLASS_NAME_SEPARATOR}bar'
494491

495492

496-
@pytest.mark.skip(reason="WIP type checking implementation")
497493
def test_diff_ops():
498494
source = """
499495
def foo():
@@ -641,8 +637,7 @@ def x(self):
641637
assert not mutants
642638

643639

644-
# TODO: implement removal of inner decorators
645-
@pytest.mark.skip
640+
@pytest.mark.skip(reason="Feature not yet implemented")
646641
def test_decorated_inner_functions_mutation():
647642
source = """
648643
def foo():

0 commit comments

Comments
 (0)