Skip to content

Commit b8abcaf

Browse files
Michael0x2agvanrossum
authored andcommitted
Add --disallow-subclassing-any flag (#2100)
Add a new flag that disallows classes with a base class of type `Any`. This tends to happen when using `--silent-imports` or when using a class from a module or library that is not typechecked by mypy. Normally, mypy will silently accept subclassing Any, which can sometimes lead to discrepancies which further leads to subtle bugs. Passing in this optional flag will report an error instead, including the name of the offending base class.
1 parent 6cc3462 commit b8abcaf

File tree

5 files changed

+63
-1
lines changed

5 files changed

+63
-1
lines changed

docs/source/command_line.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ flag (or its long form ``--help``)::
1111
usage: mypy [-h] [-v] [-V] [--python-version x.y] [--platform PLATFORM]
1212
[--py2] [-s] [--silent] [--almost-silent]
1313
[--disallow-untyped-calls] [--disallow-untyped-defs]
14-
[--check-untyped-defs]
14+
[--check-untyped-defs] [--disallow-subclassing-any]
1515
[--warn-incomplete-stub] [--warn-redundant-casts]
1616
[--warn-unused-ignores] [--fast-parser] [-i] [--cache-dir DIR]
1717
[--strict-optional] [-f] [--pdb] [--use-python-path] [--stats]
@@ -251,6 +251,14 @@ Here are some more useful flags:
251251
- ``--disallow-untyped-calls`` reports an error whenever a function
252252
with type annotations calls a function defined without annotations.
253253

254+
- ``--disallow-subclassing-any`` reports an error whenever a class
255+
inherits a value of type ``Any``. This often occurs when inheriting
256+
a class that was imported from a module not typechecked by mypy while
257+
using ``--silent-imports``. Since the module is silenced, the imported
258+
class is given a type of ``Any``. By default, mypy will assume the
259+
subclass correctly inherited the base class even though that may not
260+
actually be the case. This flag makes mypy raise an error instead.
261+
254262
- ``--incremental`` is an experimental option that enables incremental
255263
type checking. When enabled, mypy caches results from previous runs
256264
to speed up type checking. Incremental mode can help when most parts

mypy/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ def process_options(args: List[str],
154154
" or with incomplete type annotations")
155155
parser.add_argument('--check-untyped-defs', action='store_true',
156156
help="type check the interior of functions without type annotations")
157+
parser.add_argument('--disallow-subclassing-any', action='store_true',
158+
help="disallow subclassing values of type 'Any' when defining classes")
157159
parser.add_argument('--warn-incomplete-stub', action='store_true',
158160
help="warn if missing type annotation in typeshed, only relevant with"
159161
" --check-untyped-defs enabled")

mypy/options.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ def __init__(self) -> None:
3232
# Type check unannotated functions
3333
self.check_untyped_defs = False
3434

35+
# Disallow subclassing values of type 'Any'
36+
self.disallow_subclassing_any = False
37+
3538
# Also check typeshed for missing annotations
3639
self.warn_incomplete_stub = False
3740

mypy/semanal.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,12 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
769769
self.fail("Cannot subclass NewType", defn)
770770
base_types.append(base)
771771
elif isinstance(base, AnyType):
772+
if self.options.disallow_subclassing_any:
773+
if isinstance(base_expr, (NameExpr, MemberExpr)):
774+
msg = "Class cannot subclass '{}' (has type 'Any')".format(base_expr.name)
775+
else:
776+
msg = "Class cannot subclass value of type 'Any'"
777+
self.fail(msg, base_expr)
772778
info.fallback_to_any = True
773779
else:
774780
self.fail('Invalid base class', base_expr)

test-data/unit/check-flags.test

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,46 @@ def f():
3636
[out]
3737
main: note: In function "f":
3838
main:2: error: Function is missing a type annotation
39+
40+
[case testSubclassingAny]
41+
# flags: --disallow-subclassing-any
42+
from typing import Any
43+
FakeClass = None # type: Any
44+
class Foo(FakeClass): pass # E: Class cannot subclass 'FakeClass' (has type 'Any')
45+
[out]
46+
47+
[case testSubclassingAnyMultipleBaseClasses]
48+
# flags: --disallow-subclassing-any
49+
from typing import Any
50+
FakeClass = None # type: Any
51+
class ActualClass: pass
52+
class Foo(ActualClass, FakeClass): pass # E: Class cannot subclass 'FakeClass' (has type 'Any')
53+
[out]
54+
55+
[case testSubclassingAnySilentImports]
56+
# flags: --disallow-subclassing-any --silent-imports
57+
# cmd: mypy -m main
58+
59+
[file main.py]
60+
from ignored_module import BaseClass
61+
class Foo(BaseClass): pass
62+
63+
[file ignored_module.py]
64+
class BaseClass: pass
65+
66+
[out]
67+
tmp/main.py:2: error: Class cannot subclass 'BaseClass' (has type 'Any')
68+
69+
[case testSubclassingAnySilentImports2]
70+
# flags: --disallow-subclassing-any --silent-imports
71+
# cmd: mypy -m main
72+
73+
[file main.py]
74+
import ignored_module
75+
class Foo(ignored_module.BaseClass): pass
76+
77+
[file ignored_module.py]
78+
class BaseClass: pass
79+
80+
[out]
81+
tmp/main.py:2: error: Class cannot subclass 'BaseClass' (has type 'Any')

0 commit comments

Comments
 (0)