Repository: python/mypy

Issue:
Repeated --package arguments are ignored

Running `mypy -m module1 -p package1 -p package2` results in only `module1` being checked.


Running `mypy -p package1 -p package2` results in only `package2` being checked.

Ideally this would check all of them, but it should at least error, rather than silently passing and tricking me into thinking my annotations are valid in `package1`
 and `package2`




Discussion:
Hm, we may be constrained by argparse here, but I thought it would keep the last one. Also IIRC there is a check that you pass exactly one of these -- I'm surprised that didn't fire here. Maybe you can have a look at the source?
You're right, I extrapolated from the problem I had to what I thought the source code would also allow.

`mypy --package a --package b` is the failing invocation, which as you remark checks only the last package.

Is there any reason not to support an arbitrary mixture of these two arguments? I can put a patch together.

Resolving patch:
diff --git a/mypy/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -419,16 +419,16 @@ def add_invertible_flag(flag: str,
 
     code_group = parser.add_argument_group(title='How to specify the code to type check')
     code_group.add_argument('-m', '--module', action='append', metavar='MODULE',
+                            default=[],
                             dest='special-opts:modules',
                             help="type-check module; can repeat for more modules")
-    # TODO: `mypy -p A -p B` currently silently ignores A
-    # (last option wins).  Perhaps -c, -m and -p could just be
-    # command-line flags that modify how we interpret self.files?
+    code_group.add_argument('-p', '--package', action='append', metavar='PACKAGE',
+                            default=[],
+                            dest='special-opts:packages',
+                            help="type-check package recursively; can be repeated")
     code_group.add_argument('-c', '--command', action='append', metavar='PROGRAM_TEXT',
                             dest='special-opts:command',
                             help="type-check program passed in as string")
-    code_group.add_argument('-p', '--package', metavar='PACKAGE', dest='special-opts:package',
-                            help="type-check all files in a directory")
     code_group.add_argument(metavar='files', nargs='*', dest='special-opts:files',
                             help="type-check given files or directories")
 
@@ -492,14 +492,13 @@ def add_invertible_flag(flag: str,
 
     # Check for invalid argument combinations.
     if require_targets:
-        code_methods = sum(bool(c) for c in [special_opts.modules,
+        code_methods = sum(bool(c) for c in [special_opts.modules + special_opts.packages,
                                              special_opts.command,
-                                             special_opts.package,
                                              special_opts.files])
         if code_methods == 0:
             parser.error("Missing target module, package, files, or command.")
         elif code_methods > 1:
-            parser.error("May only specify one of: module, package, files, or command.")
+            parser.error("May only specify one of: module/package, files, or command.")
 
     # Set build flags.
     if options.strict_optional_whitelist is not None:
@@ -525,20 +524,21 @@ def add_invertible_flag(flag: str,
         options.incremental = True
 
     # Set target.
-    if special_opts.modules:
-        options.build_type = BuildType.MODULE
-        targets = [BuildSource(None, m, None) for m in special_opts.modules]
-        return targets, options
-    elif special_opts.package:
-        if os.sep in special_opts.package or os.altsep and os.altsep in special_opts.package:
-            fail("Package name '{}' cannot have a slash in it."
-                 .format(special_opts.package))
+    if special_opts.modules + special_opts.packages:
         options.build_type = BuildType.MODULE
         lib_path = [os.getcwd()] + build.mypy_path()
+        targets = []
         # TODO: use the same cache as the BuildManager will
-        targets = build.FindModuleCache().find_modules_recursive(special_opts.package, lib_path)
-        if not targets:
-            fail("Can't find package '{}'".format(special_opts.package))
+        cache = build.FindModuleCache()
+        for p in special_opts.packages:
+            if os.sep in p or os.altsep and os.altsep in p:
+                fail("Package name '{}' cannot have a slash in it.".format(p))
+            p_targets = cache.find_modules_recursive(p, lib_path)
+            if not p_targets:
+                fail("Can't find package '{}'".format(p))
+            targets.extend(p_targets)
+        for m in special_opts.modules:
+            targets.append(BuildSource(None, m, None))
         return targets, options
     elif special_opts.command:
         options.build_type = BuildType.PROGRAM_TEXT


Test patch:
diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test
--- a/test-data/unit/cmdline.test
+++ b/test-data/unit/cmdline.test
@@ -1054,3 +1054,25 @@ def foo(x):
 [out]
 a.py:1: error: Type signature has too many arguments
 == Return code: 2
+
+[case testModulesAndPackages]
+# cmd: mypy --package p.a --package p.b --module c
+[file p/__init__.py]
+[file p/a.py]
+def foo(x):
+    # type: (int) -> str
+    return "x"
+foo("wrong")
+[file p/b/__init__.py]
+from ..a import foo
+def bar(a):
+    # type: (int) -> str
+    return foo(a)
+bar("wrong")
+[file c.py]
+import p.b
+p.b.bar("wrong")
+[out]
+p/a.py:4: error: Argument 1 to "foo" has incompatible type "str"; expected "int"
+p/b/__init__.py:5: error: Argument 1 to "bar" has incompatible type "str"; expected "int"
+c.py:2: error: Argument 1 to "bar" has incompatible type "str"; expected "int"