8000 Skip rest of file upon top-level always-false assert (#5894) · python/mypy@79dc1f0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 79dc1f0

Browse files
authored
Skip rest of file upon top-level always-false assert (#5894)
- 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 #5308
1 parent af5df38 commit 79dc1f0

File tree

4 files changed

+88
-3
lines changed

4 files changed

+88
-3
lines changed

docs/source/common_issues.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,23 @@ More specifically, mypy will understand the use of ``sys.version_info`` and
382382
else:
383383
# Other systems
384384
385+
As a special case, you can also use one of these checks in a top-level
386+
(unindented) ``assert``; this makes mypy skip the rest of the file.
387+
Example:
388+
389+
.. code-block:: python
390+
391+
import sys
392+
393+
assert sys.platform != 'win32'
394+
395+
# The rest of this file doesn't apply to Windows.
396+
397+
Some other expressions exhibit similar behavior; in particular,
398+
``typing.TYPE_CHECKING``, variables named ``MYPY``, and any variable
399+
whose name is passed to ``--always-true`` or ``--always-false``.
400+
(However, ``True`` and ``False`` are not treated specially!)
401+
385402
.. note::
386403

387404
Mypy currently does not support more complex checks, and does not assign

mypy/semanal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3805,6 +3805,10 @@ def infer_reachability_of_if_statement(s: IfStmt, options: Options) -> None:
38053805
break
38063806

38073807

3808+
def assert_will_always_fail(s: AssertStmt, options: Options) -> bool:
3809+
return infer_condition_value(s.expr, options) in (ALWAYS_FALSE, MYPY_FALSE)
3810+
3811+
38083812
def infer_condition_value(expr: Expression, options: Options) -> int:
38093813
"""Infer whether the given condition is always true/false.
38103814

mypy/semanal_pass1.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
MypyFile, SymbolTable, SymbolTableNode, Var, Block, AssignmentStmt, FuncDef, Decorator,
2525
ClassDef, TypeInfo, ImportFrom, Import, ImportAll, IfStmt, WhileStmt, ForStmt, WithStmt,
2626
TryStmt, OverloadedFuncDef, Lvalue, Context, ImportedName, LDEF, GDEF, MDEF, UNBOUND_IMPORTED,
27-
MODULE_REF, implicit_module_attrs
27+
MODULE_REF, implicit_module_attrs, AssertStmt,
2828
)
2929
from mypy.types import Type, UnboundType, UnionType, AnyType, TypeOfAny, NoneTyp, CallableType
30-
from mypy.semanal import SemanticAnalyzerPass2, infer_reachability_of_if_statement
30+
from mypy.semanal import (
31+
SemanticAnalyzerPass2, infer_reachability_of_if_statement, assert_will_always_fail,
32+
)
3133
from mypy.semanal_shared import create_indirect_imported_name
3234
from mypy.options import Options
3335
from mypy.sametypes import is_same_type
@@ -92,8 +94,14 @@ def visit_file(self, file: MypyFile, fnam: str, mod_id: str, options: Options) -
9294
v._fullname = self.sem.qualified_name(name)
9395
self.sem.globals[name] = SymbolTableNode(GDEF, v)
9496

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

98106
# Add implicit definition of literals/keywords to builtins, as we
99107
# cannot define a variable with them explicitly.

test-data/unit/check-unreachable-code.test

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,3 +623,59 @@ class Child(Parent):
623623
reveal_type(self) # E: Revealed type is '__main__.Child'
624624
return 3
625625
[builtins fixtures/isinstance.pyi]
626+
627+
[case testUnreachableAfterToplevelAssert]
628+
import sys
629+
reveal_type(0) # E: Revealed type is 'builtins.int'
630+
assert sys.platform == 'lol'
631+
reveal_type('') # No error here :-)
632+
[builtins fixtures/ops.pyi]
633+
634+
[case testUnreachableAfterToplevelAssert2]
635+
import sys
636+
reveal_type(0) # E: Revealed type is 'builtins.int'
637+
assert sys.version_info[0] == 1
638+
reveal_type('') # No error here :-)
639+
[builtins fixtures/ops.pyi]
640+
641+
[case testUnreachableAfterToplevelAssert3]
642+
reveal_type(0) # E: Revealed type is 'builtins.int'
643+
MYPY = False
644+
assert not MYPY
645+
reveal_type('') # No error here :-)
646+
[builtins fixtures/ops.pyi]
647+
648+
[case testUnreachableAfterToplevelAssert4]
649+
# flags: --always-false NOPE
650+
reveal_type(0) # E: Revealed type is 'builtins.int'
651+
NOPE = False
652+
assert NOPE
653+
reveal_type('') # No error here :-)
654+
[builtins fixtures/ops.pyi]
655+
656+
[case testUnreachableAfterToplevelAssertImport]
657+
import foo
658+
foo.bar() # E: "object" has no attribute "bar"
659+
[file foo.py]
660+
import sys
661+
assert sys.platform == 'lol'
662+
def bar() -> None: pass
663+
[builtins fixtures/ops.pyi]
664+
665+
[case testUnreachableAfterToplevelAssertImport2]
666+
# flags: --platform lol
667+
import foo
668+
foo.bar() # No error :-)
669+
[file foo.py]
670+
import sys
671+
assert sys.platform == 'lol'
672+
def bar() -> None: pass
673+
[builtins fixtures/ops.pyi]
674+
675+
[case testUnreachableAfterToplevelAssertNotInsideIf]
676+
import sys
677+
if sys.version_info[0] >= 2:
678+
assert sys.platform == 'lol'
679+
reveal_type('') # E: Revealed type is 'builtins.str'
680+
reveal_type('') # E: Revealed type is 'builtins.str'
681+
[builtins fixtures/ops.pyi]

0 commit comments

Comments
 (0)
0