{"record":{"id":"a18cff1bc6998ad9","repo":"python/cpython","slug":"htmldir-r-is-not-a-sphinx-html-output-directory","errorCode":null,"errorMessage":"{htmldir!r} is not a Sphinx HTML output directory","messagePattern":"(.+?) is not a Sphinx HTML output directory","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"Doc/tools/check-html-ids.py","lineNumber":45,"sourceCode":"    def handle_starttag(self, tag, attrs):\n        for name, value in attrs:\n            if name == 'id':\n                if not IGNORED_ID_RE.fullmatch(value):\n                    self.__ids.add(value)\n\n\ndef get_ids_from_file(path):\n    ids = set()\n    gatherer = IDGatherer(ids)\n    with path.open(encoding='utf-8') as file:\n        while chunk := file.read(4096):\n            gatherer.feed(chunk)\n    return ids\n\n\ndef gather_ids(htmldir, *, verbose_print):\n    if not htmldir.joinpath('objects.inv').exists():\n        raise ValueError(f'{htmldir!r} is not a Sphinx HTML output directory')\n\n    if sys._is_gil_enabled:\n        pool = concurrent.futures.ProcessPoolExecutor()\n    else:\n        pool = concurrent.futures.ThreadPoolExecutor()\n    tasks = {}\n    for path in htmldir.glob('**/*.html'):\n        relative_path = path.relative_to(htmldir)\n        if '_static' in relative_path.parts:\n            continue\n        if 'whatsnew' in relative_path.parts:\n            continue\n        tasks[relative_path] = pool.submit(get_ids_from_file, path=path)\n\n    ids_by_page = {}\n    for relative_path, future in tasks.items():\n        verbose_print(relative_path)\n        ids = future.result()","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/python/cpython/blob/bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6/Doc/tools/check-html-ids.py#L27-L63","documentation":"BaseEventLoop.close() (here via the proactor event loop on Windows) refuses to close a loop that is currently running. A loop may only be closed after run_forever()/run_until_complete() has returned. Closing a running loop would tear down primitives the loop still needs, so asyncio guards it with this RuntimeError.","triggerScenarios":"Calling loop.close() from inside a coroutine or callback running on that same loop; calling loop.close() on the loop's runner thread while run_forever() is still blocked; closing a loop from another thread while it runs. Common in tests that tear down in a finally block that executes before run_until_complete unwinds.","commonSituations":"Cleanup code (finally/atexit/signal handlers) that closes the loop regardless of run state; nesting mistakes where asyncio.run(...) is emulated manually and close() is invoked inside main(); pytest fixtures closing a loop that a background thread is still running.","solutions":["Move loop.close() to after the run call completes: run_until_complete(...) then loop.close()","Inside a coroutine, just return or stop the loop with loop.stop() and close it after run_forever() returns","Prefer asyncio.run(main()) which handles creation, running, and closing in the correct order","For cross-thread teardown, signal the loop to stop via loop.call_soon_threadsafe(loop.stop), join the runner thread, then close"],"exampleFix":"# before\nasync def main(loop):\n    ...\n    loop.close()  # RuntimeError: loop is running\nloop.run_until_complete(main(loop))\n# after\nasync def main():\n    ...\nloop.run_until_complete(main())\nloop.close()","handlingStrategy":"validation","validationCode":"def stop_and_close(loop):\n    if loop.is_running():\n        loop.call_soon_threadsafe(loop.stop)\n        loop_thread.join(timeout=5)\n    if not loop.is_closed():\n        loop.close()","typeGuard":null,"tryCatchPattern":"try:\n    loop.close()\nexcept RuntimeError as e:\n    if 'running event loop' in str(e):\n        loop.stop()  # close from outside after run returns\n    else:\n        raise","preventionTips":["Prefer asyncio.run() so close ordering is always correct","Never close a loop from inside its own coroutines or callbacks","Stop loops via loop.stop() and close after the run call returns","In tests, close fixtures after run_until_complete fully unwinds"],"tags":["asyncio","event-loop","lifecycle","windows"],"backgroundTag":null,"analyzedSha":"bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6","analyzedAt":"2026-08-14T22:01:13.976Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}