diff --git a/docs/source/common_issues.rst b/docs/source/common_issues.rst index 5d409754bb7f..1c73f7ae4347 100644 --- a/docs/source/common_issues.rst +++ b/docs/source/common_issues.rst @@ -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 diff --git a/mypy/semanal.py b/mypy/semanal.py index 47a797504df1..ccb3ab28e971 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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. diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py index 92018c9a223f..b8d9eba45291 100644 --- a/mypy/semanal_pass1.py +++ b/mypy/semanal_pass1.py @@ -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 @@ -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. diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 1a36e10db69f..144cf926001a 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -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]