Repository: python/mypy

Issue:
remove mypy's stub cleanliness checking and deprecate `-f`/`--dirty-stubs`
Mypy's stub cleanliness checking/enforcement has never been helpful for me.  Every time I interact with it, it just forces me to run the same command again with `-f`.

I propose we rip it out.  I don't think it makes sense to have user-facing flags for a development feature (especially not a flag as common as `-f`), and I don't think it makes sense for mypy to know about git submodules.



Discussion:
+1

--Guido (mobile)
On Jun 16, 2016 1:44 PM, "David Fisher" notifications@github.com wrote:

> Mypy's stub cleanliness checking/enforcement has never been helpful for
> me. Every time I interact with it, it just forces me to run the same
> command again with -f.
> 
> I propose we rip it out. I don't think it makes sense to have user-facing
> flags for a development feature (especially not a flag as common as -f),
> and I don't think it makes sense for mypy to know about git submodules.
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> https://github.com/python/mypy/issues/1719, or mute the thread
> https://github.com/notifications/unsubscribe/ACwrMiHo0Pdlx9gQPWfhRsirQ_IptzDVks5qMbWsgaJpZM4I3yrX
> .

is this something still being decided? if not, I'll take a stab at it.

We definitely want to do this.  Still want to take a stab at it? :)

didn't see this till now - yeah I'll work on getting something up for this!

What about adding a shell script (or .bat) for doing the submodule update to the root of the repo? The most useful thing about the stub cleanliness check for me is that it prints out the exact command needed for updating typeshed. I can never remember the exact form. I usually use my own shell script for that, but when working in a new clone of the repo I don't have the shell script always around.

Having a script that just contains `git submodule update` seems a bit like overkill to me.  I've found adding `post-checkout` and `post-merge` hooks that update the submodule to be very convenient -- @JukkaL maybe you could just add those instead?

I'm not worried about myself in particular -- I'm more worried about new contributors that haven't encountered git submodules before. They were the original motivation for adding these checks -- to make submodule updates more discoverable. If typeshed is out of date and a user doesn't know anything about submodules, they'll likely have to read the readme to look up the right command. I still don't always remember the exact command to run after having used it for several months now, and I've found the prompts somewhat helpful (though a little noisy and inconvenient as well). 

About the hooks -- I don't like magic things happening automatically behind my back, since I'm worried about them breaking in various edge cases and causing more trouble than they are worth. Also, configuring the hooks for every clone of repo feels like unnecessary effort. As for new contributors, we can't expect them to read the docs in enough detail to set up the hooks if they are only getting familiar with the project.

Having a script (called, perhaps, `update-typeshed.sh`) seem like a reasonable compromise to me:
1. It's perhaps easier to remember `./update.sh` or `./update-typeshed.sh` than `git submodule update`.
2. It's faster to type the script name than `git submodule update` (important as we need to do this a lot, as I'd rather not rely on the hooks), since I can use shell autocompletion, unlike with git subcommands.
3. Even if I can't remember the exact name of the script, I can remember that it's a shell script so I can just run `ls *.sh` and it'll be there. Not quite so with looking up things from the readme, though I can grep the right command quickly if I'm lucky.
4. The update script can also automatically clone the submodule if I forgot to use `--recurse-submodules` when cloning (again, who reads documentation).
5. If you don't care about it, the cost of having the script is minimal and you can just ignore it.

To improve discoverability, when mypy or the test runner can't find typeshed, we could print out a message like this:

```
Git submodule 'typeshed' not initialized'.

Run ./update-typeshed.sh to initialize.

IMPORTANT: Run ./update-typeshed.sh also after to each pull, branch switch, rebase and merge
           if typeshed is out of date.    <--- use bold for this
```

Another option is to leave the checks as they are now, but provide an environment variable for suppressing all the checks.

For me, a script like `./update-typeshed.sh` would actually make things less discoverable and more cumbersome than using the standard command `git submodule update`. The latter is at least a common tool shared across many projects I've worked in before; the script would be a new thing to look up every time until it becomes another fact I remember.

Even if the script happens to consist of just a line `git submodule update`, that wouldn't make the experience any simpler -- that would itself be a fact to look up, half-remember, look up again, etc., before I could rely on it.

If post-checkout and post-merge hooks are an effective individual solution, then perhaps a good solution for new contributors would be a script meant to be run in a new clone to set up a development environment, which would create those hooks. (And perhaps other steps, like install dependencies and self into a virtualenv, though more discussion would be needed on the details.)

> I can use shell autocompletion, unlike with git subcommands.

Whatever we do with this, though, I highly recommend getting shell autocompletion for git! It provides not only subcommands but also their options, and arguments like branch names. The `git` packages in Debian and Ubuntu set it up automatically, but if your environment isn't as helpful as that, the underlying code is [in upstream Git's `contrib/` tree](https://github.com/git/git/blob/master/contrib/completion/git-completion.bash) and you can add it to your config directly. Or if you're relying on homebrew, [this SO answer](http://stackoverflow.com/questions/14970728/homebrew-s-git-not-using-completion) might be useful.

+1 to what Greg said.

With regards to new contributors: there are always some things to learn when beginning to work on a new project.  I think learning to manage your typeshed version is a pretty small thing in the process of contributing to mypy.  (And, for what it's worth, I've been running with `git submodule update` as a git hook for the past month or so, and I haven't had a single problem with it so far.  I think it's a viable option.)

Also, the dirty stub check comes with costs:
- Code complexity: Without this, there's no reason for mypy to know or care about what sort of repository it or typeshed is in.  Mypy is quite a complex piece of software already, and I think it's decently important for the long-term (and our own development velocity) that we strongly pursue simplification where possible.
- Usage complexity: the `-f`/`--dirty-stubs` portion of the help message is completely irrelevant to the majority of mypy users.


Resolving patch:
diff --git a/mypy/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -33,8 +33,6 @@ def main(script_path: str) -> None:
     sources, options = process_options(sys.argv[1:])
     if options.pdb:
         set_drop_into_pdb(True)
-    if not options.dirty_stubs:
-        git.verify_git_integrity_or_abort(build.default_data_dir(bin_dir))
     f = sys.stdout
     try:
         res = type_check_only(sources, bin_dir, options)
@@ -143,8 +141,6 @@ def parse_version(v: str) -> Tuple[int, int]:
                         const=defaults.PYTHON2_VERSION, help="use Python 2 mode")
     parser.add_argument('-s', '--silent-imports', action='store_true',
                         help="don't follow imports to .py files")
-    parser.add_argument('--silent', action='store_true', dest='special-opts:silent',
-                        help="deprecated name for --silent-imports")
     parser.add_argument('--almost-silent', action='store_true',
                         help="like --silent-imports but reports the imports as errors")
     parser.add_argument('--disallow-untyped-calls', action='store_true',
@@ -175,17 +171,21 @@ def parse_version(v: str) -> Tuple[int, int]:
     parser.add_argument('--strict-optional', action='store_true',
                         dest='special-opts:strict_optional',
                         help="enable experimental strict Optional checks")
-    parser.add_argument('-f', '--dirty-stubs', action='store_true',
-                        help="don't warn if typeshed is out of sync")
     parser.add_argument('--pdb', action='store_true', help="invoke pdb on fatal error")
-    parser.add_argument('--use-python-path', action='store_true',
-                        dest='special-opts:use_python_path',
-                        help="an anti-pattern")
     parser.add_argument('--stats', action='store_true', dest='dump_type_stats', help="dump stats")
     parser.add_argument('--inferstats', action='store_true', dest='dump_inference_stats',
                         help="dump type inference stats")
     parser.add_argument('--custom-typing', metavar='MODULE', dest='custom_typing_module',
                         help="use a custom typing module")
+    # deprecated options
+    parser.add_argument('--silent', action='store_true', dest='special-opts:silent',
+                        help=argparse.SUPPRESS)
+    parser.add_argument('-f', '--dirty-stubs', action='store_true',
+                        dest='special-opts:dirty_stubs',
+                        help=argparse.SUPPRESS)
+    parser.add_argument('--use-python-path', action='store_true',
+                        dest='special-opts:use_python_path',
+                        help=argparse.SUPPRESS)
 
     report_group = parser.add_argument_group(
         title='report generation',
@@ -231,11 +231,15 @@ def parse_version(v: str) -> Tuple[int, int]:
                      "See https://github.com/python/mypy/issues/1411 for more discussion."
                      )
 
-    # --silent is deprecated; warn about this.
+    # warn about deprecated options
     if special_opts.silent:
         print("Warning: --silent is deprecated; use --silent-imports",
               file=sys.stderr)
         options.silent_imports = True
+    if special_opts.dirty_stubs:
+        print("Warning: -f/--dirty-stubs is deprecated and no longer necessary. Mypy no longer "
+              "checks the git status of stubs.",
+              file=sys.stderr)
 
     # Check for invalid argument combinations.
     code_methods = sum(bool(c) for c in [special_opts.modules,
diff --git a/mypy/options.py b/mypy/options.py
--- a/mypy/options.py
+++ b/mypy/options.py
@@ -43,7 +43,6 @@ def __init__(self) -> None:
         self.pdb = False
         self.dump_type_stats = False
         self.dump_inference_stats = False
-        self.dirty_stubs = False
 
         # -- test options --
         # Stop after the semantic analysis phase
diff --git a/runtests.py b/runtests.py
--- a/runtests.py
+++ b/runtests.py
@@ -27,7 +27,7 @@ def get_versions():  # type: () -> typing.List[str]
 from typing import Dict, List, Optional, Set, Iterable
 
 from mypy.waiter import Waiter, LazySubprocess
-from mypy import git, util
+from mypy import util
 
 import itertools
 import os
@@ -76,7 +76,7 @@ def add_mypy_cmd(self, name: str, mypy_args: List[str], cwd: Optional[str] = Non
         full_name = 'check %s' % name
         if not self.allow(full_name):
             return
-        args = [sys.executable, self.mypy, '-f'] + mypy_args
+        args = [sys.executable, self.mypy] + mypy_args
         self.waiter.add(LazySubprocess(full_name, args, cwd=cwd, env=self.env))
 
     def add_mypy(self, name: str, *args: str, cwd: Optional[str] = None) -> None:
@@ -325,7 +325,6 @@ def main() -> None:
     blacklist = []  # type: List[str]
     arglist = []  # type: List[str]
     list_only = False
-    dirty_stubs = False
 
     allow_opts = True
     curlist = whitelist
@@ -350,8 +349,6 @@ def main() -> None:
                 curlist = arglist
             elif a == '-l' or a == '--list':
                 list_only = True
-            elif a == '-f' or a == '--dirty-stubs':
-                dirty_stubs = True
             elif a == '-h' or a == '--help':
                 usage(0)
             else:
@@ -370,9 +367,6 @@ def main() -> None:
     driver = Driver(whitelist=whitelist, blacklist=blacklist, arglist=arglist,
             verbosity=verbosity, parallel_limit=parallel_limit, xfail=[])
 
-    if not dirty_stubs:
-        git.verify_git_integrity_or_abort(driver.cwd)
-
     driver.prepend_path('PATH', [join(driver.cwd, 'scripts')])
     driver.prepend_path('MYPYPATH', [driver.cwd])
     driver.prepend_path('PYTHONPATH', [driver.cwd])


Test patch:
diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py
--- a/mypy/test/testcmdline.py
+++ b/mypy/test/testcmdline.py
@@ -46,8 +46,7 @@ def test_python_evaluation(testcase: DataDrivenTestCase) -> None:
     args = parse_args(testcase.input[0])
     # Type check the program.
     fixed = [python3_path,
-             os.path.join(testcase.old_cwd, 'scripts', 'mypy'),
-             '-f']
+             os.path.join(testcase.old_cwd, 'scripts', 'mypy')]
     process = subprocess.Popen(fixed + args,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT,
diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py
--- a/mypy/test/testpythoneval.py
+++ b/mypy/test/testpythoneval.py
@@ -56,11 +56,11 @@ def test_python_evaluation(testcase):
             # Skip, can't find a Python 2 interpreter.
             raise SkipTestCaseException()
         interpreter = python2_interpreter
-        args = ['--py2', '-f']
+        args = ['--py2']
         py2 = True
     else:
         interpreter = python3_path
-        args = ['-f']
+        args = []
         py2 = False
     # Write the program to a file.
     program = '_program.py'