Skip to content
Open
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
130 changes: 92 additions & 38 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -4620,18 +4620,7 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
# type-checking this is already done and results should be empty here.
if not manager.workers:
assert not results
for id, result in results.items():
# Interface and implementation results may be mixed in the same batch
# from different workers, process each one accordingly.
if result.interface_hash is not None:
new_hash = bytes.fromhex(result.interface_hash)
if new_hash != graph[id].interface_hash:
graph[id].mark_interface_stale()
graph[id].interface_hash = new_hash
else:
manager.flush_errors(
manager.errors.simplify_path(graph[id].xpath), result.error_lines, False
)
process_results(results, graph, manager)
ready = []
for done_scc in done:
for dependent in done_scc.direct_dependents:
Expand All @@ -4643,6 +4632,26 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
manager.trace(f"Transitive deps cache size: {sys.getsizeof(manager.transitive_deps_cache)}")


def process_results(results: dict[str, ModuleResult], graph: Graph, manager: BuildManager) -> None:
"""Process results of type-checking given modules.

This will update interface hashes and flush type-checking errors (if any).
Blockers should have been already handled by the caller.
"""
for id, result in results.items():
# Interface and implementation results may be mixed in the same batch
# from different workers, process each one accordingly.
if result.interface_hash is not None:
new_hash = bytes.fromhex(result.interface_hash)
if new_hash != graph[id].interface_hash:
graph[id].mark_interface_stale()
graph[id].interface_hash = new_hash
else:
manager.flush_errors(
manager.errors.simplify_path(graph[id].xpath), result.error_lines, False
)


def order_ascc(graph: Graph, ascc: AbstractSet[str], pri_max: int = PRI_INDIRECT) -> list[str]:
"""Come up with the ideal processing order within an SCC.

Expand Down Expand Up @@ -4760,7 +4769,45 @@ def maybe_load_deps(graph: Graph, ascc: SCC, manager: BuildManager) -> None:


def process_stale_scc(graph: Graph, ascc: SCC, manager: BuildManager) -> None:
"""Process the modules in one SCC from source code."""
"""Process the modules in one SCC from source code.

This will process module interfaces first (when possible). This mirrors
how things are done in parallel type checking.
"""
if not manager.options.local_partial_types:
# If local partial types are disabled we must process each file sequentially.
process_stale_scc_full(graph, ascc, manager)
return
manager.parse_all([graph[id] for id in ascc.mod_ids], post_parse=False)
scc_result = process_stale_scc_interface(
graph, ascc, manager, from_cache={id for id in ascc.mod_ids if graph[id].meta}
)
manager.commit()

# Process interface results before starting implementations
# (to mimic parallel checking 1:1).
mod_results = {}
stale = []
meta_files = []
for id, mod_result, meta_file in scc_result:
stale.append(id)
mod_results[id] = mod_result
meta_files.append(meta_file)
process_results(mod_results, graph, manager)

mod_results = {}
for id, meta_file in zip(stale, meta_files):
mod_results |= process_stale_scc_implementation(graph, [id], manager, [meta_file])
manager.commit()
process_results(mod_results, graph, manager)


def process_stale_scc_full(graph: Graph, ascc: SCC, manager: BuildManager) -> None:
"""Process the modules in one SCC from source code.

This is the legacy function that processes each file sequentially (line-by-line),
thus it may interleave processing interface and implementation parts.
"""
# First verify if all transitive dependencies are loaded in the current process.
t0 = time.time()
maybe_load_deps(graph, ascc, manager)
Expand Down Expand Up @@ -4863,7 +4910,7 @@ def process_stale_scc(graph: Graph, ascc: SCC, manager: BuildManager) -> None:

def process_stale_scc_interface(
graph: Graph, ascc: SCC, manager: BuildManager, from_cache: set[str]
) -> list[tuple[str, ModuleResult, str]]:
) -> list[tuple[str, ModuleResult, str | None]]:
"""Process the modules' interfaces in one SCC from source code."""
# First verify if all transitive dependencies are loaded in the current process.
t0 = time.time()
Expand Down Expand Up @@ -4909,16 +4956,19 @@ def process_stale_scc_interface(
for id in stale:
meta_tuple = meta_tuples[id]
if meta_tuple is None:
continue
meta, meta_file = meta_tuple
meta = meta_file = None
else:
meta, meta_file = meta_tuple
state = graph[id]
meta.dep_hashes = [
graph[dep].interface_hash
for dep in state.dependencies
if state.priorities.get(dep) != PRI_INDIRECT
]
write_cache_meta(meta, manager, meta_file)
manager.commit_module(meta_file)
if meta is not None:
assert meta_file is not None
meta.dep_hashes = [
graph[dep].interface_hash
for dep in state.dependencies
if state.priorities.get(dep) != PRI_INDIRECT
]
write_cache_meta(meta, manager, meta_file)
manager.commit_module(meta_file)
scc_result.append((id, ModuleResult(graph[id].interface_hash.hex(), []), meta_file))
manager.done_sccs.add(ascc.id)
manager.add_stats(
Expand All @@ -4932,7 +4982,7 @@ def process_stale_scc_interface(


def process_stale_scc_implementation(
graph: Graph, stale: list[str], manager: BuildManager, meta_files: list[str]
graph: Graph, stale: list[str], manager: BuildManager, meta_files: list[str | None]
) -> dict[str, ModuleResult]:
"""Process implementations (top-level function/method bodies) in an SCC."""
t0 = time.time()
Expand All @@ -4947,7 +4997,10 @@ def process_stale_scc_implementation(
continue
# We need to reset deferral count after possibly deferring any methods that
# are considered part of the top-level (because they define/infer variables).
checker.pass_num = 0
# Note we need to add one pass to compensate for function bodies not visited in
# type_check_first_pass(). So with current DEFAULT_LAST_PASS = 2 each function
# will be visited at most three times, for both single-phase and two-phase logic.
checker.pass_num = -1
checker.deferred_nodes.clear()
tree = graph[id].tree
assert tree is not None
Expand Down Expand Up @@ -4977,27 +5030,28 @@ def process_stale_scc_implementation(
scc_result = {}
for id, meta_file in zip(stale, meta_files):
state = graph[id]
# If there are no errors, only write the cache, don't send anything back
# to the caller (as a micro-optimization).
if graph[id].xpath not in manager.errors.ignored_files:
errors = manager.errors.file_messages(graph[id].xpath)
formatted = manager.errors.format_messages(
graph[id].xpath, errors, formatter=manager.error_formatter
)
scc_result[id] = ModuleResult(None, formatted)
else:
errors = []
if meta_file is None:
continue
indirect = [dep for dep in state.dependencies if state.priorities.get(dep) == PRI_INDIRECT]
meta_ex = CacheMetaEx(
dependencies=indirect,
suppressed=[
dep for dep in state.suppressed if state.priorities.get(dep) == PRI_INDIRECT
],
dep_hashes=[graph[dep].interface_hash for dep in indirect],
error_lines=[],
error_lines=errors,
)
if graph[id].xpath not in manager.errors.ignored_files:
errors = manager.errors.file_messages(graph[id].xpath)
formatted = manager.errors.format_messages(
graph[id].xpath, errors, formatter=manager.error_formatter
)
meta_ex.error_lines = errors
write_cache_meta_ex(meta_file, meta_ex, manager)
scc_result[id] = ModuleResult(None, formatted)
else:
# If there are no errors, only write the cache, don't send anything back
# to the caller (as a micro-optimization).
write_cache_meta_ex(meta_file, meta_ex, manager)
write_cache_meta_ex(meta_file, meta_ex, manager)
manager.commit_module(meta_file)

manager.add_stats(type_check_time_implementation=time.time() - t0)
Expand Down
11 changes: 11 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4782,6 +4782,10 @@ def analyze_member_lvalue(
self.type.names[lval.name] = SymbolTableNode(MDEF, v, implicit=True)
for func in self.scope.functions:
func.def_or_infer_vars = True

if self.is_self_member_ref(lval) or self.is_cls_member_ref(lval):
assert self.type, "Self or cls member outside a class"
cur_node = self.type.names.get(lval.name)
if (
cur_node
and isinstance(cur_node.node, Var)
Expand All @@ -4799,6 +4803,13 @@ def is_self_member_ref(self, memberexpr: MemberExpr) -> bool:
node = memberexpr.expr.node
return isinstance(node, Var) and node.is_self

def is_cls_member_ref(self, memberexpr: MemberExpr) -> bool:
"""Does memberexpr to refer to an attribute of cls?"""
if not isinstance(memberexpr.expr, NameExpr):
return False
node = memberexpr.expr.node
return isinstance(node, Var) and node.is_cls

def check_lvalue_validity(self, node: Expression | SymbolNode | None, ctx: Context) -> None:
if isinstance(node, TypeVarExpr):
self.fail("Invalid assignment target", ctx)
Expand Down
3 changes: 2 additions & 1 deletion mypy/test/testcmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import os
import re
import shlex
import subprocess
import sys
import sysconfig
Expand Down Expand Up @@ -135,7 +136,7 @@ def parse_args(line: str) -> list[str]:
m = re.match("# cmd: mypy (.*)$", line)
if not m:
return [] # No args; mypy will spit out an error.
return m.group(1).split()
return shlex.split(m.group(1))


def parse_cwd(line: str) -> str | None:
Expand Down
14 changes: 14 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -9727,3 +9727,17 @@ def f() -> None:
class X:
...
undefined # E: Name "undefined" is not defined

[case testPartialNoneTypeClassMethod]
# flags: --local-partial-types

class C:
x = None

@classmethod
def foo(cls) -> None:
if not cls.x:
cls.x = 1

reveal_type(C.x) # N: Revealed type is "builtins.int | None"
[builtins fixtures/classmethod.pyi]
16 changes: 8 additions & 8 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -2921,8 +2921,8 @@ def mix(fs: List[Callable[[S], T]]) -> Callable[[S], List[T]]:
def id(__x: U) -> U:
...
fs = [id, id, id]
reveal_type(mix(fs)) # N: Revealed type is "def [S] (S`2) -> builtins.list[S`2]"
reveal_type(mix([id, id, id])) # N: Revealed type is "def [S] (S`4) -> builtins.list[S`4]"
reveal_type(mix(fs)) # N: Revealed type is "def [S] (S`1) -> builtins.list[S`1]"
reveal_type(mix([id, id, id])) # N: Revealed type is "def [S] (S`3) -> builtins.list[S`3]"
[builtins fixtures/list.pyi]

[case testInferenceAgainstGenericCurry]
Expand Down Expand Up @@ -3098,14 +3098,14 @@ I = TypeVar("I", bound=int)
def dec4_bound(f: Callable[[I], List[T]]) -> Callable[[I], T]:
...

reveal_type(dec1(lambda x: x)) # N: Revealed type is "def [T] (T`3) -> builtins.list[T`3]"
reveal_type(dec2(lambda x: x)) # N: Revealed type is "def [S] (S`5) -> builtins.list[S`5]"
reveal_type(dec3(lambda x: x[0])) # N: Revealed type is "def [S] (S`8) -> S`8"
reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`11) -> S`11"
reveal_type(dec1(lambda x: x)) # N: Revealed type is "def [T] (T`1) -> builtins.list[T`1]"
reveal_type(dec2(lambda x: x)) # N: Revealed type is "def [S] (S`3) -> builtins.list[S`3]"
reveal_type(dec3(lambda x: x[0])) # N: Revealed type is "def [S] (S`6) -> S`6"
reveal_type(dec4(lambda x: [x])) # N: Revealed type is "def [S] (S`9) -> S`9"
reveal_type(dec1(lambda x: 1)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]"
reveal_type(dec5(lambda x: x)) # N: Revealed type is "def (builtins.int) -> builtins.list[builtins.int]"
reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`19) -> builtins.list[S`19]"
reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`23]) -> T`23"
reveal_type(dec3(lambda x: x)) # N: Revealed type is "def [S] (S`17) -> builtins.list[S`17]"
reveal_type(dec4(lambda x: x)) # N: Revealed type is "def [T] (builtins.list[T`21]) -> T`21"
dec4_bound(lambda x: x) # E: Value of type variable "I" of "dec4_bound" cannot be "list[T]"
[builtins fixtures/list.pyi]

Expand Down
29 changes: 2 additions & 27 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -2782,32 +2782,7 @@ x = '' # E: Incompatible types in assignment (expression has type "str", variab
def g() -> None:
reveal_type(x) # N: Revealed type is "builtins.int | None"

-- TODO: combine 4 tests below back into 2 when possible.
[case testLocalPartialTypesWithGlobalInitializedToNone4_no_parallel]
# flags: --local-partial-types --no-strict-optional
a = None

def f() -> None:
reveal_type(a) # N: Revealed type is "None"

reveal_type(a) # N: Revealed type is "None"
a = ''
reveal_type(a) # N: Revealed type is "builtins.str"
[builtins fixtures/list.pyi]

[case testLocalPartialTypesWithGlobalInitializedToNone5_no_parallel]
# flags: --local-partial-types
a = None

def f() -> None:
reveal_type(a) # N: Revealed type is "None"

reveal_type(a) # N: Revealed type is "None"
a = ''
reveal_type(a) # N: Revealed type is "builtins.str"
[builtins fixtures/list.pyi]

[case testLocalPartialTypesWithGlobalInitializedToNone4_parallel_only]
[case testLocalPartialTypesWithGlobalInitializedToNone4]
# flags: --local-partial-types --no-strict-optional
a = None

Expand All @@ -2819,7 +2794,7 @@ a = ''
reveal_type(a) # N: Revealed type is "builtins.str"
[builtins fixtures/list.pyi]

[case testLocalPartialTypesWithGlobalInitializedToNone5_parallel_only]
[case testLocalPartialTypesWithGlobalInitializedToNone5]
# flags: --local-partial-types
a = None

Expand Down
8 changes: 4 additions & 4 deletions test-data/unit/check-plugin-attrs.test
Original file line number Diff line number Diff line change
Expand Up @@ -990,10 +990,10 @@ class C(A, B): pass
@attr.s
class D(A): pass

reveal_type(A.__lt__) # N: Revealed type is "def [_AT] (self: _AT`29, other: _AT`29) -> builtins.bool"
reveal_type(B.__lt__) # N: Revealed type is "def [_AT] (self: _AT`30, other: _AT`30) -> builtins.bool"
reveal_type(C.__lt__) # N: Revealed type is "def [_AT] (self: _AT`31, other: _AT`31) -> builtins.bool"
reveal_type(D.__lt__) # N: Revealed type is "def [_AT] (self: _AT`32, other: _AT`32) -> builtins.bool"
reveal_type(A.__lt__) # N: Revealed type is "def [_AT] (self: _AT`5, other: _AT`5) -> builtins.bool"
reveal_type(B.__lt__) # N: Revealed type is "def [_AT] (self: _AT`6, other: _AT`6) -> builtins.bool"
reveal_type(C.__lt__) # N: Revealed type is "def [_AT] (self: _AT`7, other: _AT`7) -> builtins.bool"
reveal_type(D.__lt__) # N: Revealed type is "def [_AT] (self: _AT`8, other: _AT`8) -> builtins.bool"

A() < A()
B() < B()
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -2314,8 +2314,8 @@ class A:

@classmethod
def other_meth(cls) -> Self:
reveal_type(cls.meth) # N: Revealed type is "def [Self <: __main__.A] (self: Self`1) -> Self`1"
reveal_type(A.meth) # N: Revealed type is "def [Self <: __main__.A] (self: Self`2) -> Self`2"
reveal_type(cls.meth) # N: Revealed type is "def [Self <: __main__.A] (self: Self`2) -> Self`2"
reveal_type(A.meth) # N: Revealed type is "def [Self <: __main__.A] (self: Self`3) -> Self`3"
return cls().meth()

class B:
Expand Down
5 changes: 5 additions & 0 deletions test-data/unit/cmdline.test
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,11 @@ pass
error: Cache must be enabled in parallel mode
== Return code: 2

[case testCodeModeInParallelMode]
# cmd: mypy -c 'def foo() -> None: 42 + "no"' --num-workers=2
[out]
<string>:1: error: Unsupported operand types for + ("int" and "str")

[case testCheckingStubPackagesWorksInParallelMode]
# cmd: mypy foo-stubs --num-workers=4
[file foo-stubs/__init__.pyi]
Expand Down
Loading