mutmut 2.4.4__tar.gz → 2.4.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,3 +28,11 @@ Credits
28
28
  * Daniel Versoza Alves <daniel.alves@voxy.com>
29
29
  * Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
30
30
  * Jan Królikowski <yanekk86@gmail.com>
31
+ * Tal Amuyal <talamuyal@gmail.com>
32
+ * Eivind Jahren <eivind.jahren@gmail.com>
33
+ * WiredNerd <pbuschmail-gitlab@yahoo.com>
34
+ * Kees Hink <kees@fourdigits.nl>
35
+ * Jim Rybarski <jim@rybarski.com>
36
+ * Christopher J. Brody <chris.brody+brodybits@gmail.com>
37
+ * Century-ss <hgfc7543ggfdch@gmail.com>
38
+ * Michael Champagne <michael.champagne@elapsetech.com>
@@ -1,6 +1,17 @@
1
1
  Changelog
2
2
  ---------
3
3
 
4
+ 2.4.5
5
+ ~~~~~
6
+
7
+ * Add skipped column in html index report
8
+
9
+ * Untested/skipped in show all does not show skipped
10
+
11
+ * mutmut should pick up the config before running the original tests. That allows to specify test suits you want to run
12
+
13
+ * improved some high-level help text & documentation
14
+
4
15
  2.4.4
5
16
  ~~~~~
6
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mutmut
3
- Version: 2.4.4
3
+ Version: 2.4.5
4
4
  Summary: mutation testing for Python 3
5
5
  Home-page: https://github.com/boxed/mutmut
6
6
  Author: Anders Hovmöller
@@ -56,16 +56,24 @@ Description: mutmut - python mutation tester
56
56
 
57
57
  This will by default run pytest (or unittest if pytest is unavailable)
58
58
  on tests in the "tests" or "test" folder and
59
- it will try to figure out where the code to mutate lies. Run
59
+ it will try to figure out where the code to mutate lies.
60
+
61
+ NOTE that mutmut will apply the mutations directly, one at a time;
62
+ it is **highly** recommended to add all changes to source control
63
+ before running.
64
+
65
+ Enter
60
66
 
61
67
  .. code-block:: console
62
68
 
63
- mutmut --help
69
+ mutmut run --help
64
70
 
65
71
  for the available flags, to use other runners, etc. The recommended way to use
66
- mutmut if the defaults aren't working for you is to add a block in ``setup.cfg``.
72
+ mutmut if the defaults aren't working for you is to add a
73
+ block in ``setup.cfg`` or ``project.toml``.
67
74
  Then when you come back to mutmut weeks later you don't have to figure out the
68
- flags again, just run ``mutmut run`` and it works. Like this:
75
+ flags again, just run ``mutmut run`` and it works.
76
+ Like this in ``setup.cfg``:
69
77
 
70
78
  .. code-block:: ini
71
79
 
@@ -76,6 +84,14 @@ Description: mutmut - python mutation tester
76
84
  tests_dir=tests/
77
85
  dict_synonyms=Struct, NamedStruct
78
86
 
87
+ or like this in ``pyproject.toml``:
88
+
89
+ .. code-block:: ini
90
+
91
+ [tool.mutmut]
92
+ paths_to_mutate="src"
93
+ runner="python -m hammett -x"
94
+
79
95
  To use multiple paths either in the ``paths_to_mutate`` or ``tests_dir`` option
80
96
  use a comma or colon separated list. For example:
81
97
 
@@ -94,11 +110,11 @@ Description: mutmut - python mutation tester
94
110
  all mutants for a specific file with ``mutmut show path/to/file.py`` or all mutants
95
111
  with ``mutmut show all``.
96
112
 
97
-
98
113
  You can also write a mutant to disk with ``mutmut apply 3``. You should **REALLY**
99
114
  have the file you mutate under source code control and committed before you apply
100
115
  a mutant!
101
116
 
117
+ To generate a HTML report for a web browser: ``mutmut html``
102
118
 
103
119
  Whitelisting
104
120
  ------------
@@ -48,16 +48,24 @@ You can get started with a simple:
48
48
 
49
49
  This will by default run pytest (or unittest if pytest is unavailable)
50
50
  on tests in the "tests" or "test" folder and
51
- it will try to figure out where the code to mutate lies. Run
51
+ it will try to figure out where the code to mutate lies.
52
+
53
+ NOTE that mutmut will apply the mutations directly, one at a time;
54
+ it is **highly** recommended to add all changes to source control
55
+ before running.
56
+
57
+ Enter
52
58
 
53
59
  .. code-block:: console
54
60
 
55
- mutmut --help
61
+ mutmut run --help
56
62
 
57
63
  for the available flags, to use other runners, etc. The recommended way to use
58
- mutmut if the defaults aren't working for you is to add a block in ``setup.cfg``.
64
+ mutmut if the defaults aren't working for you is to add a
65
+ block in ``setup.cfg`` or ``project.toml``.
59
66
  Then when you come back to mutmut weeks later you don't have to figure out the
60
- flags again, just run ``mutmut run`` and it works. Like this:
67
+ flags again, just run ``mutmut run`` and it works.
68
+ Like this in ``setup.cfg``:
61
69
 
62
70
  .. code-block:: ini
63
71
 
@@ -68,6 +76,14 @@ flags again, just run ``mutmut run`` and it works. Like this:
68
76
  tests_dir=tests/
69
77
  dict_synonyms=Struct, NamedStruct
70
78
 
79
+ or like this in ``pyproject.toml``:
80
+
81
+ .. code-block:: ini
82
+
83
+ [tool.mutmut]
84
+ paths_to_mutate="src"
85
+ runner="python -m hammett -x"
86
+
71
87
  To use multiple paths either in the ``paths_to_mutate`` or ``tests_dir`` option
72
88
  use a comma or colon separated list. For example:
73
89
 
@@ -86,11 +102,11 @@ grouped by file. You can now look at a specific mutant diff with ``mutmut show 3
86
102
  all mutants for a specific file with ``mutmut show path/to/file.py`` or all mutants
87
103
  with ``mutmut show all``.
88
104
 
89
-
90
105
  You can also write a mutant to disk with ``mutmut apply 3``. You should **REALLY**
91
106
  have the file you mutate under source code control and committed before you apply
92
107
  a mutant!
93
108
 
109
+ To generate a HTML report for a web browser: ``mutmut html``
94
110
 
95
111
  Whitelisting
96
112
  ------------
@@ -1,4 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+
2
4
  import fnmatch
3
5
  import itertools
4
6
  import multiprocessing
@@ -10,6 +12,7 @@ import sys
10
12
  import toml
11
13
  from configparser import ConfigParser
12
14
  from copy import copy as copy_obj
15
+ from dataclasses import dataclass, field
13
16
  from functools import wraps
14
17
  from io import (
15
18
  open,
@@ -25,11 +28,12 @@ from threading import (
25
28
  Thread,
26
29
  )
27
30
  from time import time
31
+ from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple
28
32
 
29
33
  from parso import parse
30
34
  from parso.python.tree import Name, Number, Keyword, FStringStart, FStringEnd
31
35
 
32
- __version__ = '2.4.4'
36
+ __version__ = '2.4.5'
33
37
 
34
38
 
35
39
  if os.getcwd() not in sys.path:
@@ -40,21 +44,12 @@ except ImportError:
40
44
  mutmut_config = None
41
45
 
42
46
 
43
- class RelativeMutationID(object):
44
- def __init__(self, line, index, line_number, filename=None):
45
- self.line = line
46
- self.index = index
47
- self.line_number = line_number
48
- self.filename = filename
49
-
50
- def __repr__(self):
51
- return 'MutationID(line="{}", index={}, line_number={}, filename={})'.format(self.line, self.index, self.line_number, self.filename)
52
-
53
- def __eq__(self, other):
54
- return (self.line, self.index, self.line_number) == (other.line, other.index, other.line_number)
55
-
56
- def __hash__(self):
57
- return hash((self.line, self.index, self.line_number))
47
+ @dataclass(frozen=True)
48
+ class RelativeMutationID:
49
+ line: str
50
+ index: int
51
+ line_number: int
52
+ filename: Optional[str] = field(default=None, compare=False, hash=False)
58
53
 
59
54
 
60
55
  ALL = RelativeMutationID(filename='%all%', line='%all%', index=-1, line_number=-1)
@@ -64,7 +59,7 @@ class InvalidASTPatternException(Exception):
64
59
  pass
65
60
 
66
61
 
67
- class ASTPattern(object):
62
+ class ASTPattern:
68
63
  def __init__(self, source, **definitions):
69
64
  if definitions is None:
70
65
  definitions = {}
@@ -288,12 +283,10 @@ def lambda_mutation(children, **_):
288
283
  NEWLINE = {'formatting': [], 'indent': '', 'type': 'endl', 'value': ''}
289
284
 
290
285
 
291
- def argument_mutation(children, context, **_):
286
+ def argument_mutation(children, context: Context, **_):
292
287
  """Mutate the arguments one by one from dict(a=b) to dict(aXXX=b).
293
288
 
294
289
  This is similar to the mutation of dict literals in the form {'a': b}.
295
-
296
- :type context: Context
297
290
  """
298
291
  if len(context.stack) >= 3 and context.stack[-3].type in ('power', 'atom_expr'):
299
292
  stack_pos_of_power_node = -3
@@ -474,7 +467,7 @@ mutations_by_type = {
474
467
  # TODO: detect regexes and mutate them in nasty ways? Maybe mutate all strings as if they are regexes
475
468
 
476
469
 
477
- def should_exclude(context, config):
470
+ def should_exclude(context, config: Optional[Config]):
478
471
  if config is None or config.covered_lines_by_filename is None:
479
472
  return False
480
473
 
@@ -495,8 +488,16 @@ def should_exclude(context, config):
495
488
  return False
496
489
 
497
490
 
498
- class Context(object):
499
- def __init__(self, source=None, mutation_id=ALL, dict_synonyms=None, filename=None, config=None, index=0):
491
+ class Context:
492
+ def __init__(
493
+ self,
494
+ source: Optional[str] = None,
495
+ mutation_id=ALL,
496
+ dict_synonyms=None,
497
+ filename=None,
498
+ config: Optional[Config] = None,
499
+ index=0,
500
+ ):
500
501
  self.index = index
501
502
  self.remove_newline_at_end = False
502
503
  self._source = None
@@ -562,11 +563,9 @@ class Context(object):
562
563
  return self.mutation_id in (ALL, self.mutation_id_of_current_index)
563
564
 
564
565
 
565
- def mutate(context):
566
+ def mutate(context: Context) -> Tuple[str, int]:
566
567
  """
567
- :type context: Context
568
568
  :return: tuple of mutated source code and number of mutations performed
569
- :rtype: Tuple[str, int]
570
569
  """
571
570
  try:
572
571
  result = parse(context.source, error_recovery=False)
@@ -590,10 +589,7 @@ def mutate(context):
590
589
  return mutated_source, len(context.performed_mutation_ids)
591
590
 
592
591
 
593
- def mutate_node(node, context):
594
- """
595
- :type context: Context
596
- """
592
+ def mutate_node(node, context: Context):
597
593
  context.stack.append(node)
598
594
  try:
599
595
  if node.type in ('tfpdef', 'import_from', 'import_name'):
@@ -665,10 +661,7 @@ def mutate_node(node, context):
665
661
  context.stack.pop()
666
662
 
667
663
 
668
- def mutate_list_of_nodes(node, context):
669
- """
670
- :type context: Context
671
- """
664
+ def mutate_list_of_nodes(node, context: Context):
672
665
  return_annotation_started = False
673
666
 
674
667
  for child_node in node.children:
@@ -688,22 +681,13 @@ def mutate_list_of_nodes(node, context):
688
681
  return
689
682
 
690
683
 
691
- def list_mutations(context):
692
- """
693
- :type context: Context
694
- """
684
+ def list_mutations(context: Context):
695
685
  assert context.mutation_id == ALL
696
686
  mutate(context)
697
687
  return context.performed_mutation_ids
698
688
 
699
689
 
700
- def mutate_file(backup, context):
701
- """
702
- :type backup: bool
703
- :type context: Context
704
-
705
- :return: Tuple[str, str]
706
- """
690
+ def mutate_file(backup: bool, context: Context) -> Tuple[str, str]:
707
691
  with open(context.filename) as f:
708
692
  original = f.read()
709
693
  if backup:
@@ -715,7 +699,13 @@ def mutate_file(backup, context):
715
699
  return original, mutated
716
700
 
717
701
 
718
- def queue_mutants(*, progress, config, mutants_queue, mutations_by_file):
702
+ def queue_mutants(
703
+ *,
704
+ progress: Progress,
705
+ config: Config,
706
+ mutants_queue,
707
+ mutations_by_file: Dict[str, List[RelativeMutationID]],
708
+ ):
719
709
  from mutmut.cache import get_cached_mutation_statuses
720
710
 
721
711
  try:
@@ -810,7 +800,9 @@ def run_mutation(context: Context, callback) -> str:
810
800
  return BAD_TIMEOUT
811
801
 
812
802
  time_elapsed = time() - start
813
- if not survived and time_elapsed > config.test_time_base + (config.baseline_time_elapsed * config.test_time_multipler):
803
+ if not survived and time_elapsed > config.test_time_base + (
804
+ config.baseline_time_elapsed * config.test_time_multiplier
805
+ ):
814
806
  return OK_SUSPICIOUS
815
807
 
816
808
  if survived:
@@ -830,31 +822,31 @@ def run_mutation(context: Context, callback) -> str:
830
822
  callback(result)
831
823
 
832
824
 
833
- class Config(object):
834
- def __init__(self, swallow_output, test_command, covered_lines_by_filename,
835
- baseline_time_elapsed, test_time_multiplier, test_time_base,
836
- dict_synonyms, total, using_testmon,
837
- tests_dirs, hash_of_tests, pre_mutation, post_mutation,
838
- coverage_data, paths_to_mutate, mutation_types_to_apply, no_progress, ci, rerun_all):
839
- self.swallow_output = swallow_output
840
- self.test_command = self._default_test_command = test_command
841
- self.covered_lines_by_filename = covered_lines_by_filename
842
- self.baseline_time_elapsed = baseline_time_elapsed
843
- self.test_time_multipler = test_time_multiplier
844
- self.test_time_base = test_time_base
845
- self.dict_synonyms = dict_synonyms
846
- self.total = total
847
- self.using_testmon = using_testmon
848
- self.tests_dirs = tests_dirs
849
- self.hash_of_tests = hash_of_tests
850
- self.post_mutation = post_mutation
851
- self.pre_mutation = pre_mutation
852
- self.coverage_data = coverage_data
853
- self.paths_to_mutate = paths_to_mutate
854
- self.mutation_types_to_apply = mutation_types_to_apply
855
- self.no_progress = no_progress
856
- self.ci = ci
857
- self.rerun_all = rerun_all
825
+ @dataclass
826
+ class Config:
827
+ swallow_output: bool
828
+ test_command: str
829
+ _default_test_command: str = field(init=False)
830
+ covered_lines_by_filename: Optional[Dict[str, set[Optional[int]]]]
831
+ baseline_time_elapsed: float
832
+ test_time_multiplier: float
833
+ test_time_base: float
834
+ dict_synonyms: List[str]
835
+ total: int
836
+ using_testmon: bool
837
+ tests_dirs: List[str]
838
+ hash_of_tests: str
839
+ post_mutation: str
840
+ pre_mutation: str
841
+ coverage_data: Dict[str, Dict[int, List[str]]]
842
+ paths_to_mutate: List[str]
843
+ mutation_types_to_apply: Set[str]
844
+ no_progress: bool
845
+ ci: bool
846
+ rerun_all: bool
847
+
848
+ def __post_init__(self):
849
+ self._default_test_command = self.test_command
858
850
 
859
851
 
860
852
  def tests_pass(config: Config, callback) -> bool:
@@ -923,11 +915,9 @@ def status_printer():
923
915
  return p
924
916
 
925
917
 
926
- def guess_paths_to_mutate():
927
- """Guess the path to source code to mutate
928
918
 
929
- :rtype: str
930
- """
919
+ def guess_paths_to_mutate() -> str:
920
+ """Guess the path to source code to mutate"""
931
921
  this_dir = os.getcwd().split(os.sep)[-1]
932
922
  if isdir('lib'):
933
923
  return 'lib'
@@ -950,7 +940,7 @@ def guess_paths_to_mutate():
950
940
  'section.')
951
941
 
952
942
 
953
- class Progress(object):
943
+ class Progress:
954
944
  def __init__(self, total, output_legend, no_progress=False):
955
945
  self.total = total
956
946
  self.output_legend = output_legend
@@ -1009,24 +999,18 @@ def get_mutations_by_file_from_cache(mutation_pk):
1009
999
  return {filename: [mutation_id]}
1010
1000
 
1011
1001
 
1012
- def popen_streaming_output(cmd, callback, timeout=None):
1002
+ def popen_streaming_output(
1003
+ cmd: str, callback: Callable[[str], None], timeout: Optional[float] = None
1004
+ ) -> int:
1013
1005
  """Open a subprocess and stream its output without hard-blocking.
1014
1006
 
1015
1007
  :param cmd: the command to execute within the subprocess
1016
- :type cmd: str
1017
-
1018
1008
  :param callback: function that intakes the subprocess' stdout line by line.
1019
1009
  It is called for each line received from the subprocess' stdout stream.
1020
- :type callback: Callable[[Context], bool]
1021
-
1022
1010
  :param timeout: the timeout time of the subprocess
1023
- :type timeout: float
1024
-
1025
1011
  :raises TimeoutError: if the subprocess' execution time exceeds
1026
1012
  the timeout time
1027
-
1028
1013
  :return: the return code of the executed subprocess
1029
- :rtype: int
1030
1014
  """
1031
1015
  if os.name == 'nt': # pragma: no cover
1032
1016
  process = subprocess.Popen(
@@ -1088,7 +1072,7 @@ def popen_streaming_output(cmd, callback, timeout=None):
1088
1072
  return process.returncode
1089
1073
 
1090
1074
 
1091
- def hammett_tests_pass(config, callback):
1075
+ def hammett_tests_pass(config: Config, callback) -> bool:
1092
1076
  # noinspection PyUnresolvedReferences
1093
1077
  from hammett import main_cli
1094
1078
  modules_before = set(sys.modules.keys())
@@ -1143,12 +1127,12 @@ def hammett_tests_pass(config, callback):
1143
1127
 
1144
1128
  CYCLE_PROCESS_AFTER = 100
1145
1129
 
1146
- def run_mutation_tests(config, progress, mutations_by_file):
1147
- """
1148
- :type config: Config
1149
- :type progress: Progress
1150
- :type mutations_by_file: dict[str, list[RelativeMutationID]]
1151
- """
1130
+
1131
+ def run_mutation_tests(
1132
+ config: Config,
1133
+ progress: Progress,
1134
+ mutations_by_file: Dict[str, List[RelativeMutationID]],
1135
+ ):
1152
1136
  from mutmut.cache import update_mutant_status
1153
1137
 
1154
1138
  # Need to explicitly use the spawn method for python < 3.8 on macOS
@@ -1211,10 +1195,9 @@ def run_mutation_tests(config, progress, mutations_by_file):
1211
1195
  update_mutant_status(file_to_mutate=filename, mutation_id=mutation_id, status=status, tests_hash=config.hash_of_tests)
1212
1196
 
1213
1197
 
1214
- def read_coverage_data():
1198
+ def read_coverage_data() -> Dict[str, Dict[int, List[str]]]:
1215
1199
  """
1216
1200
  Reads the coverage database and returns a dictionary which maps the filenames to the covered lines and their contexts.
1217
- :rtype: dict[str, dict[int, list[str]]]
1218
1201
  """
1219
1202
  try:
1220
1203
  # noinspection PyPackageRequirements,PyUnresolvedReferences
@@ -1227,7 +1210,7 @@ def read_coverage_data():
1227
1210
  return {filepath: data.contexts_by_lineno(filepath) for filepath in data.measured_files()}
1228
1211
 
1229
1212
 
1230
- def read_patch_data(patch_file_path):
1213
+ def read_patch_data(patch_file_path: str):
1231
1214
  try:
1232
1215
  # noinspection PyPackageRequirements
1233
1216
  import whatthepatch
@@ -1242,12 +1225,12 @@ def read_patch_data(patch_file_path):
1242
1225
  }
1243
1226
 
1244
1227
 
1245
- def add_mutations_by_file(mutations_by_file, filename, dict_synonyms, config):
1246
- """
1247
- :type mutations_by_file: dict[str, list[RelativeMutationID]]
1248
- :type filename: str
1249
- :type dict_synonyms: list[str]
1250
- """
1228
+ def add_mutations_by_file(
1229
+ mutations_by_file: Dict[str, List[RelativeMutationID]],
1230
+ filename: str,
1231
+ dict_synonyms: List[str],
1232
+ config: Optional[Config],
1233
+ ):
1251
1234
  with open(filename) as f:
1252
1235
  source = f.read()
1253
1236
  context = Context(
@@ -1260,27 +1243,28 @@ def add_mutations_by_file(mutations_by_file, filename, dict_synonyms, config):
1260
1243
  try:
1261
1244
  mutations_by_file[filename] = list_mutations(context)
1262
1245
  from mutmut.cache import register_mutants
1246
+
1263
1247
  register_mutants(mutations_by_file)
1264
1248
  except Exception as e:
1265
- raise RuntimeError('Failed while creating mutations for {}, for line "{}"'.format(context.filename, context.current_source_line)) from e
1249
+ raise RuntimeError(
1250
+ 'Failed while creating mutations for {}, for line "{}"'.format(
1251
+ context.filename, context.current_source_line
1252
+ )
1253
+ ) from e
1266
1254
 
1267
1255
 
1268
- def python_source_files(path, tests_dirs, paths_to_exclude=None):
1256
+ def python_source_files(
1257
+ path: str, tests_dirs: List[str], paths_to_exclude: Optional[List[str]] = None
1258
+ ) -> Iterator[str]:
1269
1259
  """Attempt to guess where the python source files to mutate are and yield
1270
1260
  their paths
1271
1261
 
1272
1262
  :param path: path to a python source file or package directory
1273
- :type path: str
1274
-
1275
1263
  :param tests_dirs: list of directory paths containing test files
1276
1264
  (we do not want to mutate these!)
1277
- :type tests_dirs: list[str]
1278
-
1279
1265
  :param paths_to_exclude: list of UNIX filename patterns to exclude
1280
- :type paths_to_exclude: list[str]
1281
1266
 
1282
1267
  :return: generator listing the paths to the python source files to mutate
1283
- :rtype: Generator[str, None, None]
1284
1268
  """
1285
1269
  paths_to_exclude = paths_to_exclude or []
1286
1270
  if isdir(path):
@@ -1297,10 +1281,12 @@ def python_source_files(path, tests_dirs, paths_to_exclude=None):
1297
1281
  yield path
1298
1282
 
1299
1283
 
1300
- def compute_exit_code(progress, exception=None, ci=False):
1284
+ def compute_exit_code(
1285
+ progress: Progress, exception: Optional[Exception] = None, ci: bool = False
1286
+ ) -> int:
1301
1287
  """Compute an exit code for mutmut mutation testing
1302
1288
 
1303
- The following exit codes are available for mutmut:
1289
+ The following exit codes are available for mutmut (as documented for the CLI run command):
1304
1290
  * 0 if all mutants were killed (OK_KILLED)
1305
1291
  * 1 if a fatal error occurred
1306
1292
  * 2 if one or more mutants survived (BAD_SURVIVED)
@@ -1314,14 +1300,10 @@ def compute_exit_code(progress, exception=None, ci=False):
1314
1300
  1 for a fatal error or 0 for any other case.
1315
1301
 
1316
1302
  :param exception:
1317
- :type exception: Exception
1318
1303
  :param progress:
1319
- :type progress: Progress
1320
1304
  :param ci:
1321
- :type ci: bool
1322
1305
 
1323
1306
  :return: integer noting the exit code of the mutation tests.
1324
- :rtype: int
1325
1307
  """
1326
1308
  code = 0
1327
1309
  if exception is not None: