Repository navigation
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "checked N source files" summary line varies with the bundled typeshed version, which would make the test fail spuriously after a typeshed sync. Start the daemon with --no-error-summary so the test asserts only on the diagnostics that the fix actually affects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
|
bump code LGTM 👍 |
|
Tried to test your PR and was having difficulties, was only pure python and was too slow to complete a dmypy check before timeout and was in the process of trying to figure out how to do the mypyc build before I gave up and forgot about this. I looked at the code and agree it's probably fine but then again I don't know a whole lot about how mypy internals work. |
|
Figured out how to compile mypy from scratch and got a crash testing out your branch by checking a file once, then installing missing dependencies and re-checking it again: Error running mypy: Daemon crashed!
Traceback (most recent call last):
File "mypy/dmypy_server.py", line 243, in serve
resp = self.run_command(command, data)
File "mypy/dmypy_server.py", line 292, in run_command
ret = method(self, **data)
File "mypy/dmypy_server.py", line 370, in cmd_check
return self.check(sources, export_types, is_tty, terminal_width)
File "mypy/dmypy_server.py", line 439, in check
messages = self.fine_grained_increment_follow_imports(
File "mypy/dmypy_server.py", line 689, in fine_grained_increment_follow_imports
messages = fine_grained_manager.update(new_unsuppressed, [], followed=True)
File "mypy/server/update.py", line 287, in update
changed_modules = propagate_changes_using_dependencies(
File "mypy/server/update.py", line 894, in propagate_changes_using_dependencies
triggered |= reprocess_nodes(manager, graph, id, nodes, deps, processed_targets)
File "mypy/server/update.py", line 1060, in reprocess_nodes
graph[module_id].verify_dependencies()
File "mypy/build.py", line 3664, in verify_dependencies
find_module_and_diagnose(
File "mypy/build.py", line 3798, in find_module_and_diagnose
module_not_found(manager, caller_line, caller_state, id, result)
File "mypy/build.py", line 3948, in module_not_found
manager.error(line, msg.format(module=target), code=code)
File "mypy/build.py", line 1563, in error
self.errors.report(line, column, msg, code, blocker=blocker, only_once=only_once)
File "mypy/errors.py", line 680, in report
self.add_error_info(info)
File "mypy/errors.py", line 810, in add_error_info
self._add_error_info(file, info)
File "mypy/errors.py", line 684, in _add_error_info
assert file not in self.flushed_files
AssertionErrorStartup command: dmypy --status-file="/home/<my username>/.idlerc/mypy/dmypy.json" start --log-file="/home/<my username>/.idlerc/mypy/log.txt" --timeout=1800 -- --show-error-codes --fixed-format-cache --soft-error-limit=-1 --strict --hide-error-context --show-error-end --disallow-untyped-defs --no-color-output --show-absolute-path --show-column-numbers --warn-redundant-casts --no-implicit-reexport --disallow-untyped-calls --cache-dir="/home/<my username>/.idlerc/mypy" --warn-unreachable --warn-unused-ignores --show-traceback --no-error-summaryCheck Command: dmypy --status-file='/home/<my username>/.idlerc/mypy/dmypy.json' check --export-types '/home/<my username>/Desktop/Github-Clones/AImidterm/src/character_sim/server.py'Install packages command: uv pip install httpx2 quart quart-trio2nd check command identical to first one. Not a minimal reproducer at all, but if you would like the exact code I am checking, you can find it here In the process of recreating this error, observed the same crash also happens when re-checking after I uninstalled the packages that were installed previously. |
Fixes #20279.
Root cause
fix_module_deps()runs at the end of every follow-imports increment andre-derives each state's edges so that
state.dependenciesonly ever containsIDs still present in the graph — anything dropped is moved to
state.suppressed. It does not rewritestate.ancestors.find_reachable_changed_modules()traversesstate.dependencies + ancestorsand dereferences
graph[dep]unconditionally. Because ancestors are neverpruned, a stale ancestor ID survives into the next command and crashes the
daemon.
Concretely, with
pkg.testsexcluded from the build, the sequence is:dmypy recheck(first) dropspkg.testsfrom the graph as unreached, butpkg.tests.test_somethingkeepsancestors = ['pkg.tests'].fix_module_deps()cleansdependencies/suppressedand leaves thatdangling ancestor edge in place.
dmypy recheck(second) walks the edge and raisesKeyError: 'pkg.tests'.That two-step sequencing is why the issue reports the crash on repeated
rechecks rather than the first one.
The change
Skip IDs that are no longer in the graph when traversing dependency and
ancestor edges.
direct_imports()gets the same guard: it performs theidentical
graph[dep]dereference, and it is called from inside thefollow-imports worklist loop after
fine_grained_manager.update()calls thatcan remove modules and before
fix_module_deps()runs at the end of theincrement, so its input is not guaranteed clean either. I could not construct a
repro for that path, so that half is defensive symmetry rather than a
demonstrated crash — happy to drop it if you would prefer the minimal diff.
A deeper alternative would be to have
fix_module_deps()pruneancestorstheway it prunes
dependencies. I did not do that here becauseancestorsalsofeeds
refresh_suppressed_submodules(), so pruning it is a wider behaviouralchange than this crash warrants. Glad to take that route instead if you think
the invariant belongs there.
Test
testDaemonRecheckMissingGraphDependencyreproduces the crash via a packageexcluded with
follow_imports = skip. Verified it fails without the fix withexactly the reported
KeyError: 'pkg.tests'(return code 2) and passes withit. The daemon is started with
--no-error-summarydeliberately: thechecked N source filessummary tracks the bundled typeshed and would make thetest fail spuriously after a typeshed sync.
Note that #20279 also has a report of the same
KeyErrorreached through adifferent route (mixing relative and absolute paths across projects). This
patch fixes the dangling-edge dereference itself, so it should cover that
route too, but the test here only exercises the excluded-package case.
Checks run locally
pytest -n0 -q mypy/test/testdaemon.py— 38 passedpytest -n4 -q mypy/test/testfinegrained.py— 755 passed, 27 skippedpython runtests.py self— no issues in 341 source filespre-commit run --files mypy/dmypy_server.py test-data/unit/daemon.test— passedLLM assistance was used while investigating and preparing this change.