Skip to content

Skip rest of file upon top-level always-false assert #5894

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 5 commits into from
Nov 15, 2018
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
17 changes: 17 additions & 0 deletions docs/source/common_issues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,23 @@ More specifically, mypy will understand the use of ``sys.version_info`` and
else:
# Other systems

As a special case, you can also use one of these checks in a top-level
(unindented) ``assert``; this makes mypy skip the rest of the file.
Example:

.. code-block:: python

import sys

assert sys.platform != 'win32'

# The rest of this file doesn't apply to Windows.

Some other expressions exhibit similar behavior; in particular,
``typing.TYPE_CHECKING``, variables named ``MYPY``, and any variable
whose name is passed to ``--always-true`` or ``--always-false``.
(However, ``True`` and ``False`` are not treated specially!)

.. note::

Mypy currently does not support more complex checks, and does not assign
Expand Down
4 changes: 4 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3805,6 +3805,10 @@ def infer_reachability_of_if_statement(s: IfStmt, options: Options) -> None:
break


def assert_will_always_fail(s: AssertStmt, options: Options) -> bool:
return infer_condition_value(s.expr, options) in (ALWAYS_FALSE, MYPY_FALSE)


def infer_condition_value(expr: Expression, options: Options) -> int:
"""Infer whether the given condition is always true/false.

Expand Down
14 changes: 11 additions & 3 deletions mypy/semanal_pass1.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
MypyFile, SymbolTable, SymbolTableNode, Var, Block, AssignmentStmt, FuncDef, Decorator,
ClassDef, TypeInfo, ImportFrom, Import, ImportAll, IfStmt, WhileStmt, ForStmt, WithStmt,
TryStmt, OverloadedFuncDef, Lvalue, Context, ImportedName, LDEF, GDEF, MDEF, UNBOUND_IMPORTED,
MODULE_REF, implicit_module_attrs
MODULE_REF, implicit_module_attrs, AssertStmt,
)
from mypy.types import Type, UnboundType, UnionType, AnyType, TypeOfAny, NoneTyp, CallableType
from mypy.semanal import SemanticAnalyzerPass2, infer_reachability_of_if_statement
from mypy.semanal import (
SemanticAnalyzerPass2, infer_reachability_of_if_statement, assert_will_always_fail,
)
from mypy.semanal_shared import create_indirect_imported_name
from mypy.options import Options
from mypy.sametypes import is_same_type
Expand Down Expand Up @@ -92,8 +94,14 @@ def visit_file(self, file: MypyFile, fnam: str, mod_id: str, options: Options) -
v._fullname = self.sem.qualified_name(name)
self.sem.globals[name] = SymbolTableNode(GDEF, v)

for d in defs:
for i, d in enumerate(defs):
d.accept(self)
if isinstance(d, AssertStmt) and assert_will_always_fail(d, options):
# We've encountered an assert that's always false,
# e.g. assert sys.platform == 'lol'. Truncate the
# list of statements. This mutates file.defs too.
del defs[i + 1:]
break

# Add implicit definition of literals/keywords to builtins, as we
# cannot define a variable with them explicitly.
Expand Down
56 changes: 56 additions & 0 deletions test-data/unit/check-unreachable-code.test
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,59 @@ class Child(Parent):
reveal_type(self) # E: Revealed type is '__main__.Child'
return 3
[builtins fixtures/isinstance.pyi]

[case testUnreachableAfterToplevelAssert]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe also test the case where the assert is nested within an if statement.

import sys
reveal_type(0) # E: Revealed type is 'builtins.int'
assert sys.platform == 'lol'
reveal_type('') # No error here :-)
[builtins fixtures/ops.pyi]

[case testUnreachableAfterToplevelAssert2]
import sys
reveal_type(0) # E: Revealed type is 'builtins.int'
assert sys.version_info[0] == 1
reveal_type('') # No error here :-)
[builtins fixtures/ops.pyi]

[case testUnreachableAfterToplevelAssert3]
reveal_type(0) # E: Revealed type is 'builtins.int'
MYPY = False
assert not MYPY
reveal_type('') # No error here :-)
[builtins fixtures/ops.pyi]

[case testUnreachableAfterToplevelAssert4]
# flags: --always-false NOPE
reveal_type(0) # E: Revealed type is 'builtins.int'
NOPE = False
assert NOPE
reveal_type('') # No error here :-)
[builtins fixtures/ops.pyi]

[case testUnreachableAfterToplevelAssertImport]
import foo
foo.bar() # E: "object" has no attribute "bar"
[file foo.py]
import sys
assert sys.platform == 'lol'
def bar() -> None: pass
[builtins fixtures/ops.pyi]

[case testUnreachableAfterToplevelAssertImport2]
# flags: --platform lol
import foo
foo.bar() # No error :-)
[file foo.py]
import sys
assert sys.platform == 'lol'
def bar() -> None: pass
[builtins fixtures/ops.pyi]

[case testUnreachableAfterToplevelAssertNotInsideIf]
import sys
if sys.version_info[0] >= 2:
assert sys.platform == 'lol'
reveal_type('') # E: Revealed type is 'builtins.str'
reveal_type('') # E: Revealed type is 'builtins.str'
[builtins fixtures/ops.pyi]