Skip to content

Commit

Permalink
Skip rest of file upon top-level always-false assert (python#5894)
Browse files Browse the repository at this point in the history
- An always-false condition is a check for `sys.platform` or
  `sys.version_info` or a condition derived from `MYPY` or
 `typing.TYPE_CHECKING` or from a name passed to
  `--always-false`. Note that `assert False` doesn't count (!).

Fixes python#5308
  • Loading branch information
gvanrossum authored Nov 15, 2018
1 parent af5df38 commit 79dc1f0
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 3 deletions.
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]
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]

0 comments on commit 79dc1f0

Please sign in to comment.