Repository: python/mypy

Issue:
Add --strict for opt-in to new features
I have a codebase which is currently compatible with all the 'extra hard' checks:
`--strict-optional --warn-unused-ignores --warn-redundant-casts --check-untyped-defs --disallow-untyped-defs --disallow-untyped-calls`

I guess in future mypy will add future parameters like this, and I'd like our CI pipeline to automatically opt in to enabling them, so this functionality isn't missed.

Therefore, I'd like a `--strict` option which enables all the above options, and support for `--no-*` to disable specific options on a case-by-case basis if we find a new option isn't desirable.

Thanks!


Discussion:
I don't think I'd want this for myself -- our CI would just break each time we were to upgrade mypy. But maybe someone who really wants this feature can submit a PR.

Resolving patch:
diff --git a/mypy/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -126,6 +126,29 @@ def __init__(self, prog: Optional[str]) -> None:
         super().__init__(prog=prog, max_help_position=28)
 
 
+# Define pairs of flag prefixes with inverse meaning.
+flag_prefix_pairs = [
+    ('allow', 'disallow'),
+    ('show', 'hide'),
+]
+flag_prefix_map = {}  # type: Dict[str, str]
+for a, b in flag_prefix_pairs:
+    flag_prefix_map[a] = b
+    flag_prefix_map[b] = a
+
+
+def invert_flag_name(flag: str) -> str:
+    split = flag[2:].split('-', 1)
+    if len(split) == 2:
+        prefix, rest = split
+        if prefix in flag_prefix_map:
+            return '--{}-{}'.format(flag_prefix_map[prefix], rest)
+        elif prefix == 'no':
+            return '--{}'.format(rest)
+
+    return '--no-{}'.format(flag[2:])
+
+
 def process_options(args: List[str],
                     require_targets: bool = True
                     ) -> Tuple[List[BuildSource], Options]:
@@ -135,6 +158,32 @@ def process_options(args: List[str],
                                      fromfile_prefix_chars='@',
                                      formatter_class=AugmentedHelpFormatter)
 
+    strict_flag_names = []  # type: List[str]
+    strict_flag_assignments = []  # type: List[Tuple[str, bool]]
+
+    def add_invertible_flag(flag: str,
+                            *,
+                            inverse: str = None,
+                            default: bool,
+                            dest: str = None,
+                            help: str,
+                            strict_flag: bool = False
+                            ) -> None:
+        if inverse is None:
+            inverse = invert_flag_name(flag)
+        arg = parser.add_argument(flag,  # type: ignore  # incorrect stub for add_argument
+                                  action='store_false' if default else 'store_true',
+                                  dest=dest,
+                                  help=help + " (inverse: {})".format(inverse))
+        dest = arg.dest
+        arg = parser.add_argument(inverse,  # type: ignore  # incorrect stub for add_argument
+                                  action='store_true' if default else 'store_false',
+                                  dest=dest,
+                                  help=argparse.SUPPRESS)
+        if strict_flag:
+            strict_flag_names.append(flag)
+            strict_flag_assignments.append((dest, not default))
+
     # Unless otherwise specified, arguments will be parsed directly onto an
     # Options object.  Options that require further processing should have
     # their `dest` prefixed with `special-opts:`, which will cause them to be
@@ -154,37 +203,36 @@ def process_options(args: List[str],
                         help="silently ignore imports of missing modules")
     parser.add_argument('--follow-imports', choices=['normal', 'silent', 'skip', 'error'],
                         default='normal', help="how to treat imports (default normal)")
-    parser.add_argument('--disallow-untyped-calls', action='store_true',
+    add_invertible_flag('--disallow-untyped-calls', default=False, strict_flag=True,
                         help="disallow calling functions without type annotations"
                         " from functions with type annotations")
-    parser.add_argument('--disallow-untyped-defs', action='store_true',
+    add_invertible_flag('--disallow-untyped-defs', default=False, strict_flag=True,
                         help="disallow defining functions without type annotations"
                         " or with incomplete type annotations")
-    parser.add_argument('--check-untyped-defs', action='store_true',
+    add_invertible_flag('--check-untyped-defs', default=False, strict_flag=True,
                         help="type check the interior of functions without type annotations")
-    parser.add_argument('--disallow-subclassing-any', action='store_true',
+    add_invertible_flag('--disallow-subclassing-any', default=False, strict_flag=True,
                         help="disallow subclassing values of type 'Any' when defining classes")
-    parser.add_argument('--warn-incomplete-stub', action='store_true',
+    add_invertible_flag('--warn-incomplete-stub', default=False,
                         help="warn if missing type annotation in typeshed, only relevant with"
                         " --check-untyped-defs enabled")
-    parser.add_argument('--warn-redundant-casts', action='store_true',
+    add_invertible_flag('--warn-redundant-casts', default=False, strict_flag=True,
                         help="warn about casting an expression to its inferred type")
-    parser.add_argument('--warn-no-return', action='store_true',
+    add_invertible_flag('--warn-no-return', default=False,
                         help="warn about functions that end without returning")
-    parser.add_argument('--warn-unused-ignores', action='store_true',
+    add_invertible_flag('--warn-unused-ignores', default=False, strict_flag=True,
                         help="warn about unneeded '# type: ignore' comments")
-    parser.add_argument('--show-error-context', action='store_false',
+    add_invertible_flag('--show-error-context', default=True,
                         dest='hide_error_context',
                         help='Precede errors with "note:" messages explaining context')
-    parser.add_argument('--fast-parser', action='store_true',
-                        help="enable fast parser (recommended except on Windows)")
+    add_invertible_flag('--fast-parser', default=False,
+                        help="enable fast parser (recommended)")
     parser.add_argument('-i', '--incremental', action='store_true',
                         help="enable experimental module cache")
     parser.add_argument('--cache-dir', action='store', metavar='DIR',
                         help="store module cache info in the given folder in incremental mode "
                         "(defaults to '{}')".format(defaults.CACHE_DIR))
-    parser.add_argument('--strict-optional', action='store_true',
-                        dest='strict_optional',
+    add_invertible_flag('--strict-optional', default=False, strict_flag=True,
                         help="enable experimental strict Optional checks")
     parser.add_argument('--strict-optional-whitelist', metavar='GLOB', nargs='*',
                         help="suppress strict Optional errors in all but the provided files "
@@ -207,15 +255,17 @@ def process_options(args: List[str],
     parser.add_argument('--config-file',
                         help="Configuration file, must have a [mypy] section "
                         "(defaults to {})".format(defaults.CONFIG_FILE))
-    parser.add_argument('--show-column-numbers', action='store_true',
-                        dest='show_column_numbers',
+    add_invertible_flag('--show-column-numbers', default=False,
                         help="Show column numbers in error messages")
     parser.add_argument('--find-occurrences', metavar='CLASS.MEMBER',
                         dest='special-opts:find_occurrences',
                         help="print out all usages of a class member (experimental)")
-    parser.add_argument('--strict-boolean', action='store_true',
-                        dest='strict_boolean',
+    add_invertible_flag('--strict-boolean', default=False, strict_flag=True,
                         help='enable strict boolean checks in conditions')
+    strict_help = "Strict mode. Enables the following flags: {}".format(
+        ", ".join(strict_flag_names))
+    parser.add_argument('--strict', action='store_true', dest='special-opts:strict',
+                        help=strict_help)
     # hidden options
     # --shadow-file a.py tmp.py will typecheck tmp.py in place of a.py.
     # Useful for tools to make transformations to a file to get more
@@ -229,9 +279,6 @@ def process_options(args: List[str],
     parser.add_argument('--debug-cache', action='store_true', help=argparse.SUPPRESS)
     # --dump-graph will dump the contents of the graph of SCCs and exit.
     parser.add_argument('--dump-graph', action='store_true', help=argparse.SUPPRESS)
-    parser.add_argument('--hide-error-context', action='store_true',
-                        dest='hide_error_context',
-                        help=argparse.SUPPRESS)
     # deprecated options
     parser.add_argument('-f', '--dirty-stubs', action='store_true',
                         dest='special-opts:dirty_stubs',
@@ -270,7 +317,7 @@ def process_options(args: List[str],
                             help="type-check given files or directories")
 
     # Parse arguments once into a dummy namespace so we can get the
-    # filename for the config file.
+    # filename for the config file and know if the user requested all strict options.
     dummy = argparse.Namespace()
     parser.parse_args(args, dummy)
     config_file = defaults.CONFIG_FILE
@@ -284,6 +331,12 @@ def process_options(args: List[str],
     if config_file and os.path.exists(config_file):
         parse_config_file(options, config_file)
 
+    # Set strict flags before parsing (if strict mode enabled), so other command
+    # line options can override.
+    if getattr(dummy, 'special-opts:strict'):
+        for dest, value in strict_flag_assignments:
+            setattr(options, dest, value)
+
     # Parse command line for real, using a split namespace.
     special_opts = argparse.Namespace()
     parser.parse_args(args, SplitNamespace(options, special_opts, 'special-opts:'))


Test patch: