Skip to content

Add --disallow-subclassing-any flag #2100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ flag (or its long form ``--help``)::
usage: mypy [-h] [-v] [-V] [--python-version x.y] [--platform PLATFORM]
[--py2] [-s] [--silent] [--almost-silent]
[--disallow-untyped-calls] [--disallow-untyped-defs]
[--check-untyped-defs]
[--check-untyped-defs] [--disallow-subclassing-any]
[--warn-incomplete-stub] [--warn-redundant-casts]
[--warn-unused-ignores] [--fast-parser] [-i] [--cache-dir DIR]
[--strict-optional] [-f] [--pdb] [--use-python-path] [--stats]
Expand Down Expand Up @@ -251,6 +251,14 @@ Here are some more useful flags:
- ``--disallow-untyped-calls`` reports an error whenever a function
with type annotations calls a function defined without annotations.

- ``--disallow-subclassing-any`` reports an error whenever a class
inherits a value of type ``Any``. This often occurs when inheriting
a class that was imported from a module not typechecked by mypy while
using ``--silent-imports``. Since the module is silenced, the imported
class is given a type of ``Any``. By default, mypy will assume the
subclass correctly inherited the base class even though that may not
actually be the case. This flag makes mypy raise an error instead.

- ``--incremental`` is an experimental option that enables incremental
type checking. When enabled, mypy caches results from previous runs
to speed up type checking. Incremental mode can help when most parts
Expand Down
2 changes: 2 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ def process_options(args: List[str],
" or with incomplete type annotations")
parser.add_argument('--check-untyped-defs', action='store_true',
help="type check the interior of functions without type annotations")
parser.add_argument('--disallow-subclassing-any', action='store_true',
help="disallow subclassing values of type 'Any' when defining classes")
parser.add_argument('--warn-incomplete-stub', action='store_true',
help="warn if missing type annotation in typeshed, only relevant with"
" --check-untyped-defs enabled")
Expand Down
3 changes: 3 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def __init__(self) -> None:
# Type check unannotated functions
self.check_untyped_defs = False

# Disallow subclassing values of type 'Any'
self.disallow_subclassing_any = False

# Also check typeshed for missing annotations
self.warn_incomplete_stub = False

Expand Down
6 changes: 6 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,12 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
self.fail("Cannot subclass NewType", defn)
base_types.append(base)
elif isinstance(base, AnyType):
if self.options.disallow_subclassing_any:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice if this reported the name of the base class that's Any. You can probably extract that from base_expr at least in simple cases. Extra points if you can also report the module from it was imported (but I'm not sure if that info is easily available by the time we get here).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvanrossum -- whoops, I must have overlooked this comment! I've pushed a new diff that reports the name when possible. Though, as you suspected, finding the module the base class was imported from was somewhat tricky.

If you'd like, I can make the error message even more descriptive and leave a brief note explaining possible causes of this error and potential fixes.

if isinstance(base_expr, (NameExpr, MemberExpr)):
msg = "Class cannot subclass '{}' (has type 'Any')".format(base_expr.name)
else:
msg = "Class cannot subclass value of type 'Any'"
self.fail(msg, base_expr)
info.fallback_to_any = True
else:
self.fail('Invalid base class', base_expr)
Expand Down
43 changes: 43 additions & 0 deletions test-data/unit/check-flags.test
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,46 @@ def f():
[out]
main: note: In function "f":
main:2: error: Function is missing a type annotation

[case testSubclassingAny]
# flags: --disallow-subclassing-any
from typing import Any
FakeClass = None # type: Any
class Foo(FakeClass): pass # E: Class cannot subclass 'FakeClass' (has type 'Any')
[out]

[case testSubclassingAnyMultipleBaseClasses]
# flags: --disallow-subclassing-any
from typing import Any
FakeClass = None # type: Any
class ActualClass: pass
class Foo(ActualClass, FakeClass): pass # E: Class cannot subclass 'FakeClass' (has type 'Any')
[out]

[case testSubclassingAnySilentImports]
# flags: --disallow-subclassing-any --silent-imports
# cmd: mypy -m main

[file main.py]
from ignored_module import BaseClass
class Foo(BaseClass): pass

[file ignored_module.py]
class BaseClass: pass

[out]
tmp/main.py:2: error: Class cannot subclass 'BaseClass' (has type 'Any')

[case testSubclassingAnySilentImports2]
# flags: --disallow-subclassing-any --silent-imports
# cmd: mypy -m main

[file main.py]
import ignored_module
class Foo(ignored_module.BaseClass): pass

[file ignored_module.py]
class BaseClass: pass

[out]
tmp/main.py:2: error: Class cannot subclass 'BaseClass' (has type 'Any')