From 9b504468d9cb2b00424338fae2fcd402f2b869b4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 06:48:51 -0400 Subject: [PATCH 1/2] Own App and widget execution through one message-pump task lifetime --- .../README.md | 27 ++ .../before.json | 272 ++++++++++++++++++ src/textual/app.py | 81 +++--- src/textual/message_pump.py | 48 ++-- src/textual/screen.py | 1 - 5 files changed, 368 insertions(+), 61 deletions(-) create mode 100644 evidence/shared-pump-task-lifetime-20261006/README.md create mode 100644 evidence/shared-pump-task-lifetime-20261006/before.json diff --git a/evidence/shared-pump-task-lifetime-20261006/README.md b/evidence/shared-pump-task-lifetime-20261006/README.md new file mode 100644 index 0000000000..f7768eabe1 --- /dev/null +++ b/evidence/shared-pump-task-lifetime-20261006/README.md @@ -0,0 +1,27 @@ +# Shared message-pump task lifetime + +Native73 left a real-entry cycle: run_test installed App._task, while run_async +entered App's replacement _process_messages with no task enrollment. A routed +pointer's completion could then bubble into its waiting App queue. + +MessagePump._process_messages now owns actual task acquisition and release for +both App entrypoints and scheduled widget pumps. App supplies its existing startup +body through _process_messages_body. The test launcher retains only its local join +handle; Screen no longer clears task custody during its exit hook. Widget scheduling +still enrolls the original task before its coroutine starts, and never reinstalls +an eager task which has already completed. Timer/Worker/Markdown tasks are distinct +resources and stay with their original owners. + +Original pointer completion, driver coroutine admission, capture, queues, shutdown +and selection boundaries are unchanged. No missing-task fallback or new queue, +timer, Toad override or completion exemption is added. App's duplicate body context +was removed; the shared lifetime borrows its existing polymorphic _context. + +Existing refactor-audit Package parsed 249 native production and 462 test modules, +zero omissions. before.json groups all 1286 lexical task/lifetime/entrypoint sites +by source coordinates. Dynamic external subclasses remain unresolved explicitly. + +Source checkpoint. Next qualification is actual run_async/HeadlessDriver FIFO +press/move/release/wheel with bubbling to App, plus terminal task release. The old +33 native73 controls and closed physical03 are not rerun. No installed, physical, +provider, package or public runtime operation is authorized by this checkpoint. diff --git a/evidence/shared-pump-task-lifetime-20261006/before.json b/evidence/shared-pump-task-lifetime-20261006/before.json new file mode 100644 index 0000000000..e2448441fc --- /dev/null +++ b/evidence/shared-pump-task-lifetime-20261006/before.json @@ -0,0 +1,272 @@ +{ + "revision": "1a12a18675a0fa3c2a361476d53136f26c91932b", + "coverage": [ + { + "root": "src/textual", + "parsed": 249, + "omissions": [] + }, + { + "root": "tests", + "parsed": 462, + "omissions": [] + } + ], + "limits": "Lexical task names include independent Timer/Worker/Markdown owners. Dynamic external subclasses are not exhaustively resolved.", + "lexical_sites": 1286, + "consumer_coordinates": { + "src/textual/app.py::_close_messages::Attribute::Load": "3777,3787", + "src/textual/app.py::_process_messages::AsyncFunctionDef::declaration": "3384", + "src/textual/app.py::_process_messages::Attribute::Load": "2201,2296", + "src/textual/app.py::_process_messages_loop::Attribute::Load": "3484", + "src/textual/app.py::_start_messages::Attribute::Load": "3714,3747", + "src/textual/app.py::_task::Attribute::Store": "2211", + "src/textual/app.py::run_async::AsyncFunctionDef::declaration": "2230", + "src/textual/app.py::run_async::Attribute::Load": "2346", + "src/textual/app.py::run_test::AsyncFunctionDef::declaration": "2143", + "src/textual/await_remove.py::_task::Attribute::Load": "85", + "src/textual/message.py::_bubble_to::FunctionDef::declaration": "154", + "src/textual/message.py::_task::Attribute::Load": "165", + "src/textual/message_pump.py::_bubble_to::Attribute::Load": "900", + "src/textual/message_pump.py::_close_messages::AsyncFunctionDef::declaration": "570", + "src/textual/message_pump.py::_close_messages::Attribute::Load": "686", + "src/textual/message_pump.py::_message_loop_exit::AsyncFunctionDef::declaration": "635", + "src/textual/message_pump.py::_message_loop_exit::Attribute::Load": "625", + "src/textual/message_pump.py::_post_message_and_wait::FunctionDef::declaration": "911", + "src/textual/message_pump.py::_process_messages::AsyncFunctionDef::declaration": "606", + "src/textual/message_pump.py::_process_messages::Attribute::Load": "600", + "src/textual/message_pump.py::_process_messages_loop::AsyncFunctionDef::declaration": "688", + "src/textual/message_pump.py::_process_messages_loop::Attribute::Load": "614", + "src/textual/message_pump.py::_start_messages::FunctionDef::declaration": "594", + "src/textual/message_pump.py::_task::Attribute::Load": "266,267,523,525,582,590,923,933,944", + "src/textual/message_pump.py::_task::Attribute::Store": "134,599,633", + "src/textual/message_pump.py::task::FunctionDef::declaration": "265", + "src/textual/message_pump.py::wait_for_refresh::AsyncFunctionDef::declaration": "511", + "src/textual/screen.py::_message_loop_exit::AsyncFunctionDef::declaration": "1381", + "src/textual/screen.py::_message_loop_exit::Attribute::Load": "1382", + "src/textual/screen.py::_post_message_and_wait::Attribute::Load": "1830,2129", + "src/textual/screen.py::_task::Attribute::Store": "1391", + "src/textual/timer.py::_task::Attribute::Load": "96,100,118,120,122", + "src/textual/timer.py::_task::Attribute::Store": "66,91,101,125,156", + "src/textual/widget.py::_close_messages::Attribute::Load": "4751,4756", + "src/textual/widget.py::_message_loop_exit::AsyncFunctionDef::declaration": "4753", + "src/textual/widget.py::_post_message_and_wait::Attribute::Load": "4553", + "src/textual/widget.py::_start_messages::Attribute::Load": "842", + "src/textual/widget.py::_task::Attribute::Load": "4766", + "src/textual/widgets/_markdown.py::_task::Attribute::Load": "69,74,75,76", + "src/textual/widgets/_markdown.py::_task::Attribute::Store": "58,70,77", + "src/textual/worker.py::_task::Attribute::Load": "420,436,441,442,465,467", + "src/textual/worker.py::_task::Attribute::Store": "187,423", + "tests/animations/test_disabling_animations.py::run_test::Attribute::Load": "27,49,70,109,130,150", + "tests/animations/test_loading_indicator_animation.py::run_test::Attribute::Load": "14,26,38", + "tests/animations/test_progress_bar_animation.py::run_test::Attribute::Load": "21,32,43", + "tests/animations/test_scrolling_animation.py::run_test::Attribute::Load": "22,40,58,75", + "tests/animations/test_switch_animation.py::run_test::Attribute::Load": "19,38,57", + "tests/animations/test_tabs_underline_animation.py::run_test::Attribute::Load": "24,40,56", + "tests/command_palette/test_click_away.py::run_test::Attribute::Load": "22", + "tests/command_palette/test_command_source_environment.py::run_test::Attribute::Load": "33", + "tests/command_palette/test_declare_sources.py::run_test::Attribute::Load": "31,42,63,74,95", + "tests/command_palette/test_discover.py::run_test::Attribute::Load": "30", + "tests/command_palette/test_escaping.py::run_test::Attribute::Load": "22", + "tests/command_palette/test_events.py::run_test::Attribute::Load": "43,49,56,69", + "tests/command_palette/test_interaction.py::run_test::Attribute::Load": "23,33,45", + "tests/command_palette/test_no_results.py::run_test::Attribute::Load": "15", + "tests/command_palette/test_run_on_select.py::run_test::Attribute::Load": "34,54", + "tests/command_palette/test_worker_interference.py::run_test::Attribute::Load": "25,46", + "tests/css/test_css_path_key.py::run_test::Attribute::Load": "48,71,88", + "tests/css/test_css_reloading.py::run_test::Attribute::Load": "51,82", + "tests/css/test_inheritance.py::run_test::Attribute::Load": "35", + "tests/css/test_initial.py::run_test::Attribute::Load": "42,79", + "tests/css/test_nested_css.py::run_test::Attribute::Load": "40,66,89,169", + "tests/css/test_programmatic_style_changes.py::run_test::Attribute::Load": "13,41,92,132,147,183,208", + "tests/css/test_reapply_rules.py::run_test::Attribute::Load": "22", + "tests/css/test_rule_dependent_caches.py::run_test::Attribute::Load": "53", + "tests/css/test_screen_css.py::run_test::Attribute::Load": "94,117,138,159,178,199,220,244,273,302", + "tests/css/test_stylesheet.py::run_test::Attribute::Load": "23,42,66,149,186,216,267", + "tests/css/test_target_selector_matching.py::run_test::Attribute::Load": "129", + "tests/directory_tree/test_change_path.py::run_test::Attribute::Load": "16", + "tests/directory_tree/test_early_show_root.py::run_test::Attribute::Load": "14", + "tests/footer/test_footer.py::run_test::Attribute::Load": "52", + "tests/footer/test_footer.py::wait_for_refresh::Attribute::Load": "56", + "tests/frame_damage_installed_pilot.py::run_test::Attribute::Load": "11", + "tests/input/test_cut_copy_paste.py::run_test::Attribute::Load": "13,26,39", + "tests/input/test_input_clear.py::run_test::Attribute::Load": "11", + "tests/input/test_input_key_modification_actions.py::run_test::Attribute::Load": "26,36,46,55,65,80,90,100,110,120,129,145,156,166,176", + "tests/input/test_input_key_movement_actions.py::run_test::Attribute::Load": "28,36,44,53,62,71,80,89,105,115,131,140,149,168", + "tests/input/test_input_messages.py::run_test::Attribute::Load": "30,36,43,51,58,65,72", + "tests/input/test_input_mouse.py::run_test::Attribute::Load": "63,74", + "tests/input/test_input_properties.py::run_test::Attribute::Load": "28,34,40,49,59,68,84,101", + "tests/input/test_input_replace.py::run_test::Attribute::Load": "31,60", + "tests/input/test_input_restrict.py::run_test::Attribute::Load": "74,87,117,140", + "tests/input/test_input_terminal_cursor.py::run_test::Attribute::Load": "18,25", + "tests/input/test_input_validation.py::run_test::Attribute::Load": "30,49,59,78,89,112,139,166,182,213", + "tests/input/test_select_on_focus.py::run_test::Attribute::Load": "21", + "tests/layouts/test_grid.py::run_test::Attribute::Load": "29", + "tests/layouts/test_grid.py::wait_for_refresh::Attribute::Load": "31", + "tests/layouts/test_horizontal.py::run_test::Attribute::Load": "26", + "tests/listview/test_inherit_listview.py::run_test::Attribute::Load": "39,48,57", + "tests/listview/test_listview_initial_index.py::run_test::Attribute::Load": "40", + "tests/listview/test_listview_navigation.py::run_test::Attribute::Load": "31", + "tests/listview/test_listview_remove_items.py::run_test::Attribute::Load": "16,53,75,102", + "tests/notifications/test_all_levels_notifications.py::run_test::Attribute::Load": "27", + "tests/notifications/test_app_notifications.py::run_test::Attribute::Load": "12,18,26,36,47", + "tests/option_list/test_option_list_create.py::run_test::Attribute::Load": "29,38,53,61,68,77,86,95,116,126,140,163", + "tests/option_list/test_option_list_disabled.py::run_test::Attribute::Load": "31,39,47,57,67,77,87,94,101,108", + "tests/option_list/test_option_list_id_stability.py::run_test::Attribute::Load": "19", + "tests/option_list/test_option_list_mouse_click.py::run_test::Attribute::Load": "30", + "tests/option_list/test_option_list_mouse_hover.py::run_test::Attribute::Load": "23,30,39,48,60", + "tests/option_list/test_option_list_movement.py::run_test::Attribute::Load": "21,27,35,45,52,61,70,77,84,95,102,113,124,138,159,193", + "tests/option_list/test_option_list_option_subclass.py::run_test::Attribute::Load": "27", + "tests/option_list/test_option_messages.py::run_test::Attribute::Load": "39,47,57,68,78,86,97,108", + "tests/option_list/test_option_prompt_replacement.py::run_test::Attribute::Load": "21,28,35,43,51,60,69", + "tests/option_list/test_option_removal.py::run_test::Attribute::Load": "25,36,47,58,69,81,93,100,107", + "tests/select/test_blank_and_clear.py::run_test::Attribute::Load": "16,28,48,61,74", + "tests/select/test_changed_message.py::run_test::Attribute::Load": "22,32,47,63", + "tests/select/test_empty_select.py::run_test::Attribute::Load": "14,25,39,49", + "tests/select/test_prompt.py::run_test::Attribute::Load": "19,43", + "tests/select/test_remove.py::run_test::Attribute::Load": "24", + "tests/select/test_value.py::run_test::Attribute::Load": "26,33,49,56,71,83,95,108,123", + "tests/selection_list/test_over_wide_selections.py::run_test::Attribute::Load": "22", + "tests/selection_list/test_selection_click_checkbox.py::run_test::Attribute::Load": "26,35", + "tests/selection_list/test_selection_list_create.py::run_test::Attribute::Load": "34,43,52,60,77,88,104,119", + "tests/selection_list/test_selection_messages.py::run_test::Attribute::Load": "52,60,70,84,97,120,133,148,161,176,192,202,212", + "tests/selection_list/test_selection_values.py::run_test::Attribute::Load": "20,26,34,42,50,58,69,77", + "tests/shutdown_mode_installed_pilot.py::run_test::Attribute::Load": "27", + "tests/suggester/test_input_suggestions.py::run_test::Attribute::Load": "22,30,38,46,52,60,69,90,97", + "tests/test_animation.py::run_test::Attribute::Load": "26,48,68,85,106,138,177,188,197,209,251,295", + "tests/test_app.py::run_async::Attribute::Load": "397", + "tests/test_app.py::run_test::Attribute::Load": "39,105,117,124,136,142,171,200,232,253,262,295,319,348,370,427,441", + "tests/test_app_focus_blur.py::run_test::Attribute::Load": "19,29,43,55,69", + "tests/test_app_focus_styles.py::run_test::Attribute::Load": "33,57", + "tests/test_await_remove.py::run_test::Attribute::Load": "19", + "tests/test_batch_paint_admission.py::run_test::Attribute::Load": "43,87,112,180,236,258", + "tests/test_binding.py::run_test::Attribute::Load": "120,135", + "tests/test_binding_inheritance.py::run_test::Attribute::Load": "41,67,91,124,153,226,234,272,319,364,403,457,514,595,634", + "tests/test_border_subtitle.py::run_test::Attribute::Load": "15", + "tests/test_box_model_cache_lifetime.py::run_test::Attribute::Load": "11,36,54", + "tests/test_call_x_schedulers.py::run_test::Attribute::Load": "22,44", + "tests/test_closed_presentation_lifetime.py::run_test::Attribute::Load": "34,48", + "tests/test_collapsible.py::run_test::Attribute::Load": "33,46,59,73,86,98,110,134,162,184,206,228,243", + "tests/test_command.py::run_test::Attribute::Load": "58", + "tests/test_compositor.py::run_test::Attribute::Load": "26,99,143,154,185,228,284,303,339,383", + "tests/test_compositor_frame_damage.py::run_test::Attribute::Load": "13,44,98,136,172,214,269", + "tests/test_compositor_horizontal_damage.py::run_test::Attribute::Load": "41", + "tests/test_containers.py::run_test::Attribute::Load": "36,65,82,97,120", + "tests/test_content_switcher.py::run_test::Attribute::Load": "24,34,45,62,80,97,103,113", + "tests/test_count_parameters.py::run_test::Attribute::Load": "101", + "tests/test_data_bind.py::run_test::Attribute::Load": "25,85", + "tests/test_data_table.py::run_test::Attribute::Load": "70,174,185,209,223,253,263,273,282,296,305,324,333,348,359,375,386,418,428,447,456,466,476,494,505,516,525,535,551,559,578,589,604,615,629,637,652,660,675,688,698,709,719,730,756,773,784,795,824,854,879,893,911,934,955,988,1019,1055,1091,1130,1145,1203,1217,1237,1256,1281,1322,1358,1371,1400,1420,1460,1488", + "tests/test_demo.py::run_test::Attribute::Load": "9", + "tests/test_disabled.py::run_test::Attribute::Load": "50,58,67,77,88,143", + "tests/test_display_constraints.py::run_test::Attribute::Load": "10,28,50", + "tests/test_dom.py::run_test::Attribute::Load": "329", + "tests/test_driver.py::run_test::Attribute::Load": "25,61,84,107,130,170", + "tests/test_dynamic_bindings.py::run_test::Attribute::Load": "31", + "tests/test_focus.py::run_test::Attribute::Load": "36,82,96,114,126,138,151,171,189,206,250,281,301,358,379,395,414,445,501,529", + "tests/test_focus_declaration_targets.py::run_test::Attribute::Load": "27,56,84,103", + "tests/test_footer_projection_lifetime.py::run_test::Attribute::Load": "25,68,113,146", + "tests/test_frame_transaction.py::run_test::Attribute::Load": "9", + "tests/test_freeze.py::run_test::Attribute::Load": "25", + "tests/test_gc.py::run_test::Attribute::Load": "37", + "tests/test_getters.py::run_test::Attribute::Load": "27,60", + "tests/test_header.py::run_test::Attribute::Load": "18,36,54,74,92,113,134", + "tests/test_height_independent_arrangement.py::run_test::Attribute::Load": "19,69,85,99,122,137,160,176,220,238,256,281,298,326,343,366,390", + "tests/test_height_independent_measurement.py::run_test::Attribute::Load": "25,48,65,84,115,133,145,168,185,212,233,248", + "tests/test_issue_4248.py::run_test::Attribute::Load": "27", + "tests/test_keymap.py::run_test::Attribute::Load": "46,58,71,112,171,217", + "tests/test_keys.py::run_test::Attribute::Load": "39", + "tests/test_lazy.py::run_test::Attribute::Load": "18,39", + "tests/test_lazy_dom_storage.py::run_test::Attribute::Load": "42", + "tests/test_links.py::run_test::Attribute::Load": "33", + "tests/test_loading.py::run_test::Attribute::Load": "25,54", + "tests/test_log.py::run_test::Attribute::Load": "22", + "tests/test_logger.py::run_test::Attribute::Load": "40", + "tests/test_markdown.py::run_test::Attribute::Load": "92,107,124,141,162,198,217", + "tests/test_markdown_fence_cache.py::run_test::Attribute::Load": "17,42", + "tests/test_markdownviewer.py::run_test::Attribute::Load": "43,67,84", + "tests/test_masked_input.py::run_test::Attribute::Load": "33,52,63,75,100,120,157,167,176,185,195,201,210,219", + "tests/test_message_handling.py::run_test::Attribute::Load": "46", + "tests/test_message_pump.py::run_test::Attribute::Load": "87,119,156,191,211", + "tests/test_message_pump_payload_lifetime.py::run_test::Attribute::Load": "39,56", + "tests/test_modal.py::run_test::Attribute::Load": "90", + "tests/test_modal.py::wait_for_refresh::Attribute::Load": "95", + "tests/test_mount.py::_task::Attribute::Load": "83", + "tests/test_mount.py::run_test::Attribute::Load": "27,100,136", + "tests/test_native_scene_size_publication.py::run_test::Attribute::Load": "32,66,98,137", + "tests/test_node_list.py::run_test::Attribute::Load": "171,229", + "tests/test_on.py::run_test::Attribute::Load": "43,82,147,193,217,265,302,352,371", + "tests/test_overflow_change.py::run_test::Attribute::Load": "14", + "tests/test_paste.py::run_test::Attribute::Load": "14,40", + "tests/test_pilot.py::run_test::Attribute::Load": "58,74,89,104,118,128,137,221,279,310,339,354,386,393,410,424,448", + "tests/test_pointer_capture_order.py::_task::Attribute::Load": "179", + "tests/test_pointer_capture_order.py::run_test::Attribute::Load": "54,106,129,149,170", + "tests/test_progress_bar.py::run_test::Attribute::Load": "155,204", + "tests/test_query.py::run_test::Attribute::Load": "19,256,266,277,290,307,334,349,375,394", + "tests/test_reactive.py::run_test::Attribute::Load": "29,45,71,100,122,140,163,188,207,228,242,264,300,365,401,432,449,469,493,521,558,597,623,649,686,724,763,786,829", + "tests/test_reactive_watch_lifetime.py::_task::Attribute::Load": "51", + "tests/test_reactive_watch_lifetime.py::run_test::Attribute::Load": "28,46,62,88", + "tests/test_remove_transaction.py::run_test::Attribute::Load": "31,132,143", + "tests/test_removed_scene_ownership.py::run_test::Attribute::Load": "20,58,97,140,161,189,223", + "tests/test_reparent.py::_task::Attribute::Load": "38,48", + "tests/test_reparent.py::run_test::Attribute::Load": "31,62,92,107,139", + "tests/test_screen_modes.py::run_test::Attribute::Load": "83,90,99,108,115,125,132,141,151,212", + "tests/test_screens.py::run_test::Attribute::Load": "32,158,201,244,273,292,311,329,359,383,416,451,458,487,502,587,614", + "tests/test_selection.py::run_test::Attribute::Load": "44,82,107,134,159,189,245,307,333,375,399,456", + "tests/test_shutdown.py::run_test::Attribute::Load": "17", + "tests/test_signal.py::_task::Attribute::Load": "179", + "tests/test_signal.py::run_test::Attribute::Load": "39,111,131,149,171", + "tests/test_signal.py::task::Attribute::Load": "175", + "tests/test_structural_measurement_revision.py::run_test::Attribute::Load": "13,42", + "tests/test_style_importance.py::run_test::Attribute::Load": "38,49,60,66,75,83,90,100", + "tests/test_style_inheritance.py::run_test::Attribute::Load": "29", + "tests/test_subtree_geometry_cache.py::run_test::Attribute::Load": "18,34", + "tests/test_suspend.py::run_test::Attribute::Load": "11,54", + "tests/test_switch.py::run_test::Attribute::Load": "17", + "tests/test_tabbed_content.py::run_test::Attribute::Load": "25,84,131,154,183,200,220,238,259,284,300,322,348,364,387,422,457,479,494,510,526,542,561,593,615,637,653,665,677,689,712,728,744,763,785,816,842,860,883,910", + "tests/test_tabs.py::run_test::Attribute::Load": "31,43,62,76,97,122,149,174,201,218,257,290,307,330,360,382,422,428,439,456,470,484,507", + "tests/test_test_runner.py::run_test::Attribute::Load": "14", + "tests/test_timer_lifetime.py::_task::Attribute::Load": "43,48", + "tests/test_timer_lifetime.py::run_test::Attribute::Load": "38,53", + "tests/test_tooltips.py::run_test::Attribute::Load": "38,48,58,71,84,97,110", + "tests/test_unmount.py::run_test::Attribute::Load": "39", + "tests/test_viewport_geometry_targets.py::run_test::Attribute::Load": "52,95,141,176,193,213", + "tests/test_viewport_layout.py::run_test::Attribute::Load": "27", + "tests/test_visible.py::run_test::Attribute::Load": "29,71", + "tests/test_widget.py::run_test::Attribute::Load": "54,141,150,157,174,182,189,196,203,211,222,232,295,313,350,394,404,425,461,491,505,521,551,587,646,694,714,759", + "tests/test_widget_child_moving.py::run_test::Attribute::Load": "11,20,29,38,58,76,94,112,119,129,147,161,174,185", + "tests/test_widget_focus_styles.py::run_test::Attribute::Load": "43,81,112", + "tests/test_widget_measurement_commit.py::run_test::Attribute::Load": "29,56,83,94,109,122,136,152,166", + "tests/test_widget_mounting.py::run_test::Attribute::Load": "22,26,40,47,54,61,68,75,82,89,96,102,111,122", + "tests/test_widget_removing.py::run_test::Attribute::Load": "8,21,30,39,52,65,90,109,130,146,162,179,187,203,218", + "tests/test_widget_visibility.py::run_test::Attribute::Load": "29", + "tests/test_win_sleep.py::run_test::Attribute::Load": "35", + "tests/text_area/test_edit_via_api.py::run_test::Attribute::Load": "43,56,66,77,101,123,132,141,149,160,168,177,200,217,243,269,303,332,347,383,393,400,429,477,514,533,549,559", + "tests/text_area/test_edit_via_bindings.py::run_test::Attribute::Load": "41,49,73,86,97,107,129,148,160,182,209,233,268,297,325,354,366,378,407,422,437,461,475,488,518,539,564,583,611", + "tests/text_area/test_editor_state.py::run_test::Attribute::Load": "13,41", + "tests/text_area/test_escape_binding.py::run_test::Attribute::Load": "30,46", + "tests/text_area/test_history.py::run_test::Attribute::Load": "50,61,75,96,138,153,177,191,200,209,243,264,287,303,352,367,390", + "tests/text_area/test_issue_4301.py::run_test::Attribute::Load": "29", + "tests/text_area/test_languages.py::run_test::Attribute::Load": "20,37,47,56,67,85", + "tests/text_area/test_messages.py::run_test::Attribute::Load": "40,55,69,82,96", + "tests/text_area/test_selection.py::run_test::Attribute::Load": "24,31,39,50,60,69,85,100,114,124,140,157,173,192,224,254,271,292,307,327,347", + "tests/text_area/test_selection_bindings.py::run_test::Attribute::Load": "34,43,51,59,78,87,96,106,118,128,139,148,160,170,180,191,204,216,241,251,264,278,296,306", + "tests/text_area/test_setting_themes.py::run_test::Attribute::Load": "17,29,39,48", + "tests/text_area/test_textarea_cut_copy_paste.py::run_test::Attribute::Load": "13,26,39", + "tests/toggles/test_checkbox.py::run_test::Attribute::Load": "25,37", + "tests/toggles/test_labels.py::run_test::Attribute::Load": "17", + "tests/toggles/test_radiobutton.py::run_test::Attribute::Load": "29,45", + "tests/toggles/test_radioset.py::run_test::Attribute::Load": "32,42,51,67,76,90,110,118,135,162", + "tests/tree/test_directory_tree.py::run_test::Attribute::Load": "39,65,98,148,199", + "tests/tree/test_node_refresh.py::run_test::Attribute::Load": "35,41,50,59", + "tests/tree/test_tree_availability.py::run_test::Attribute::Load": "60,76,92,113", + "tests/tree/test_tree_clearing.py::run_test::Attribute::Load": "48,59,70,80,88,99,108", + "tests/tree/test_tree_cursor.py::run_test::Attribute::Load": "46,62,77,91,110,136,158,171,189", + "tests/tree/test_tree_expand_etc.py::run_test::Attribute::Load": "24,35,47,59,72,88", + "tests/tree/test_tree_messages.py::run_test::Attribute::Load": "57,69,81,92,102,112,124,135,146,157,168,239,249", + "tests/workers/test_work_decorator.py::run_test::Attribute::Load": "45,179", + "tests/workers/test_worker.py::_task::Attribute::Load": "173", + "tests/workers/test_worker.py::run_test::Attribute::Load": "25,58,117,134,155,180,203,219,232,244,259", + "tests/workers/test_worker_context.py::run_test::Attribute::Load": "29,53,73", + "tests/workers/test_worker_manager.py::run_test::Attribute::Load": "45,86,123" + } +} diff --git a/src/textual/app.py b/src/textual/app.py index 0e77548554..c75df1c0fe 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2208,7 +2208,7 @@ async def run_app(app: App[ReturnType]) -> None: # Launch the app in the "background" - self._task = app_task = create_task(run_app(app), name=f"run_test {app}") + app_task = create_task(run_app(app), name=f"run_test {app}") # Wait until the app has performed all startup routines. await app_ready_event.wait() @@ -3381,7 +3381,7 @@ async def _init_devtools(self): except DevtoolsConnectionError: self.log.system(f"Couldn't connect to devtools ( {self.devtools.url} )") - async def _process_messages( + async def _process_messages_body( self, ready_callback: CallbackType | None = None, headless: bool = False, @@ -3492,51 +3492,50 @@ async def invoke_ready_callback() -> None: finally: await Timer._stop_all(self._timers) - with self._context(): - if not await app_prelude(): - return - self._running = True - try: - load_event = events.Load() - await self._dispatch_message(load_event) + if not await app_prelude(): + return + self._running = True + try: + load_event = events.Load() + await self._dispatch_message(load_event) - driver = self._driver = self._build_driver( - headless=headless, - inline=inline, - mouse=mouse, - size=terminal_size, - ) - self.log(driver=driver) + driver = self._driver = self._build_driver( + headless=headless, + inline=inline, + mouse=mouse, + size=terminal_size, + ) + self.log(driver=driver) - if not self._exit: - driver.start_application_mode() - try: - with redirect_stdout(self._capture_stdout): - with redirect_stderr(self._capture_stderr): - await run_process_messages() + if not self._exit: + driver.start_application_mode() + try: + with redirect_stdout(self._capture_stdout): + with redirect_stderr(self._capture_stderr): + await run_process_messages() - finally: - Reactive._clear_watchers(self) - if self._driver.is_inline: - cursor_x, cursor_y = self._previous_cursor_position + finally: + Reactive._clear_watchers(self) + if self._driver.is_inline: + cursor_x, cursor_y = self._previous_cursor_position + self._driver.write( + Control.move(-cursor_x, -cursor_y).segment.text + ) + self._driver.flush() + if inline_no_clear and not self.app._exit_renderables: + console = Console() + try: + console.print(self.screen._compositor) + except ScreenStackError: + console.print() + else: self._driver.write( - Control.move(-cursor_x, -cursor_y).segment.text + Control.move(0, -self.INLINE_PADDING).segment.text ) - self._driver.flush() - if inline_no_clear and not self.app._exit_renderables: - console = Console() - try: - console.print(self.screen._compositor) - except ScreenStackError: - console.print() - else: - self._driver.write( - Control.move(0, -self.INLINE_PADDING).segment.text - ) - driver.stop_application_mode() - except Exception as error: - self._handle_exception(error) + driver.stop_application_mode() + except Exception as error: + self._handle_exception(error) async def _pre_process(self) -> bool: """Special case for the app, which doesn't need the functionality in MessagePump.""" diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index f2a7094164..3f95116f78 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -596,33 +596,23 @@ def _start_messages(self) -> None: self._thread_init() if self.app._running: - self._task = create_task( + task = create_task( self._process_messages(), name=f"message pump {self}" ) + # Enroll scheduled custody until the coroutine acquires it. An + # eager task which already exited has released that custody. + if not task.done(): + self._task = task else: self._closing = True self._closed = True - async def _process_messages(self) -> None: - self._running = True + async def _process_messages(self, **kwargs: Any) -> None: + """Own the real pump task through subclass startup, dispatch and exit.""" + self._task = asyncio.current_task() try: with self._context(): - try: - if not await self._pre_process(): - return - try: - await self._process_messages_loop() - except CancelledError: - pass - finally: - self._running = False - try: - if self._timers: - await Timer._stop_all(self._timers) - self._timers.clear() - Reactive._clear_watchers(self) - finally: - await self._message_loop_exit() + await self._process_messages_body(**kwargs) finally: # Bound methods and closures in a quiet publisher may retain this # node despite its weak subscriber key. Teardown owns their removal, @@ -632,6 +622,26 @@ async def _process_messages(self) -> None: Reactive._clear_watch_subscriptions(self) self._task = None + async def _process_messages_body(self, **kwargs: Any) -> None: + """Default widget startup and teardown inside the shared task lifetime.""" + self._running = True + try: + if not await self._pre_process(): + return + try: + await self._process_messages_loop() + except CancelledError: + pass + finally: + self._running = False + try: + if self._timers: + await Timer._stop_all(self._timers) + self._timers.clear() + Reactive._clear_watchers(self) + finally: + await self._message_loop_exit() + async def _message_loop_exit(self) -> None: """Called when the message loop has completed.""" diff --git a/src/textual/screen.py b/src/textual/screen.py index b96c383310..1efd7c377e 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -1388,7 +1388,6 @@ async def _message_loop_exit(self) -> None: self._arrangement_cache.clear() self.screen_layout_refresh_signal.unsubscribe(self) self._nodes._clear() - self._task = None def _pop_result_callback(self) -> None: """Remove the latest result callback from the stack.""" From 36ff54d957ab7733c8e29d7113b8d244c26f7a75 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 06:55:41 -0400 Subject: [PATCH 2/2] Qualify shared pump ownership through real run_async driver entry --- .../README.md | 37 ++- .../after.json | 298 ++++++++++++++++++ .../caller-task-assertion-refusal.log | 50 +++ .../collection-refusal.log | 16 + .../real-entry.log | 2 + tests/test_real_app_pump_lifetime.py | 154 +++++++++ 6 files changed, 549 insertions(+), 8 deletions(-) create mode 100644 evidence/shared-pump-task-lifetime-20261006/after.json create mode 100644 evidence/shared-pump-task-lifetime-20261006/caller-task-assertion-refusal.log create mode 100644 evidence/shared-pump-task-lifetime-20261006/collection-refusal.log create mode 100644 evidence/shared-pump-task-lifetime-20261006/real-entry.log create mode 100644 tests/test_real_app_pump_lifetime.py diff --git a/evidence/shared-pump-task-lifetime-20261006/README.md b/evidence/shared-pump-task-lifetime-20261006/README.md index f7768eabe1..774a474f78 100644 --- a/evidence/shared-pump-task-lifetime-20261006/README.md +++ b/evidence/shared-pump-task-lifetime-20261006/README.md @@ -17,11 +17,32 @@ and selection boundaries are unchanged. No missing-task fallback or new queue, timer, Toad override or completion exemption is added. App's duplicate body context was removed; the shared lifetime borrows its existing polymorphic _context. -Existing refactor-audit Package parsed 249 native production and 462 test modules, -zero omissions. before.json groups all 1286 lexical task/lifetime/entrypoint sites -by source coordinates. Dynamic external subclasses remain unresolved explicitly. - -Source checkpoint. Next qualification is actual run_async/HeadlessDriver FIFO -press/move/release/wheel with bubbling to App, plus terminal task release. The old -33 native73 controls and closed physical03 are not rerun. No installed, physical, -provider, package or public runtime operation is authorized by this checkpoint. +Existing refactor-audit Package parsed all 249 native production modules and +462 before / 463 after test modules, with zero omissions. before.json and after.json +record declaration and consumer coordinates. All App/Screen pump-task writes were +deleted; initialization, scheduled enrollment, acquisition and release now belong +to MessagePump. Independent Timer/Worker/Markdown tasks are included in lexical +results but retain their distinct owners. Dynamic external subclasses remain +unresolved explicitly. + +## Actual entrypoint qualification + +`PYTHONPATH=src PYTHONDONTWRITEBYTECODE=1 /usr/bin/python -m pytest -q +tests/test_real_app_pump_lifetime.py --tb=short`: **4 passed in 0.69s**. + +The actual run_async/HeadlessDriver ingress delivered press, move, release and +wheel in order, including target-to-App bubbling and capture release. The actual +pump task matched its caller, cleared at pump exit, and child tasks joined. Other +cases covered startup failure, cancellation and eager completion without task +reenrollment. These are source App checks using the original driver and queues. +They do not use run_test to demonstrate real-entry task ownership. + +Original collection refusal and test-assertion refusal are preserved. The latter +incorrectly assumed an inline run_async caller Task must finish when its borrowed +pump lifetime finishes. The corrected checks create and join the actual run_async +Task explicitly; no production or completion rule changed after that refusal. + +The old 33 native73 controls and closed physical03 were not rerun. No installed, +physical, provider, package or public runtime operation occurred. The remaining +real-entry acceptance is Parent's actual terminal/Toad motion journey against the +matching source; the original physical03 cause remains unproved without a stack. diff --git a/evidence/shared-pump-task-lifetime-20261006/after.json b/evidence/shared-pump-task-lifetime-20261006/after.json new file mode 100644 index 0000000000..add307e111 --- /dev/null +++ b/evidence/shared-pump-task-lifetime-20261006/after.json @@ -0,0 +1,298 @@ +{ + "revision": "d1eba9398d5d338eb1c6d5947b64c6ce8875efb8", + "coverage": [ + { + "root": "src/textual", + "parsed": 249, + "omissions": [] + }, + { + "root": "tests", + "parsed": 463, + "omissions": [] + } + ], + "limits": "Lexical task names include independent Timer/Worker/Markdown owners. Dynamic external subclasses are not exhaustively resolved.", + "lexical_sites": 1356, + "consumer_coordinates": { + "src/textual/app.py::_close_messages::Attribute::Load": "3786,3776", + "src/textual/app.py::_process_messages::Attribute::Load": "2296,2201", + "src/textual/app.py::_process_messages_body::AsyncFunctionDef::declaration": "3384", + "src/textual/app.py::_process_messages_loop::Attribute::Load": "3484", + "src/textual/app.py::_start_messages::Attribute::Load": "3746,3713", + "src/textual/app.py::run_async::AsyncFunctionDef::declaration": "2230", + "src/textual/app.py::run_async::Attribute::Load": "2346", + "src/textual/app.py::run_test::AsyncFunctionDef::declaration": "2143", + "src/textual/await_remove.py::_task::Attribute::Load": "85", + "src/textual/await_remove.py::task::Name::Load": "147,85,147,155", + "src/textual/await_remove.py::task::Name::Store": "147,85,155", + "src/textual/message.py::_bubble_to::FunctionDef::declaration": "154", + "src/textual/message.py::_task::Attribute::Load": "165", + "src/textual/message_pump.py::_bubble_to::Attribute::Load": "910", + "src/textual/message_pump.py::_close_messages::AsyncFunctionDef::declaration": "570", + "src/textual/message_pump.py::_close_messages::Attribute::Load": "696", + "src/textual/message_pump.py::_message_loop_exit::AsyncFunctionDef::declaration": "645", + "src/textual/message_pump.py::_message_loop_exit::Attribute::Load": "643", + "src/textual/message_pump.py::_post_message_and_wait::FunctionDef::declaration": "921", + "src/textual/message_pump.py::_process_messages::AsyncFunctionDef::declaration": "610", + "src/textual/message_pump.py::_process_messages::Attribute::Load": "600", + "src/textual/message_pump.py::_process_messages_body::AsyncFunctionDef::declaration": "625", + "src/textual/message_pump.py::_process_messages_body::Attribute::Load": "615", + "src/textual/message_pump.py::_process_messages_loop::AsyncFunctionDef::declaration": "698", + "src/textual/message_pump.py::_process_messages_loop::Attribute::Load": "632", + "src/textual/message_pump.py::_start_messages::FunctionDef::declaration": "594", + "src/textual/message_pump.py::_task::Attribute::Load": "267,266,523,525,582,582,933,943,954,590", + "src/textual/message_pump.py::_task::Attribute::Store": "134,612,623,605", + "src/textual/message_pump.py::task::FunctionDef::declaration": "265", + "src/textual/message_pump.py::task::Name::Load": "605,944,604,948,950", + "src/textual/message_pump.py::task::Name::Store": "599,943", + "src/textual/message_pump.py::wait_for_refresh::AsyncFunctionDef::declaration": "511", + "src/textual/pilot.py::task::Name::Load": "520", + "src/textual/pilot.py::task::Name::Store": "519", + "src/textual/rlock.py::task::Name::Load": "17,20,26,31,18", + "src/textual/rlock.py::task::Name::Store": "16,25", + "src/textual/screen.py::_message_loop_exit::AsyncFunctionDef::declaration": "1381", + "src/textual/screen.py::_message_loop_exit::Attribute::Load": "1382", + "src/textual/screen.py::_post_message_and_wait::Attribute::Load": "1829,2128", + "src/textual/timer.py::_task::Attribute::Load": "96,100,118,120,122", + "src/textual/timer.py::_task::Attribute::Store": "66,91,101,156,125", + "src/textual/widget.py::_close_messages::Attribute::Load": "4751,4756", + "src/textual/widget.py::_message_loop_exit::AsyncFunctionDef::declaration": "4753", + "src/textual/widget.py::_post_message_and_wait::Attribute::Load": "4553", + "src/textual/widget.py::_start_messages::Attribute::Load": "842", + "src/textual/widget.py::_task::Attribute::Load": "4766,4766", + "src/textual/widget.py::task::Name::Load": "175,178,175", + "src/textual/widget.py::task::Name::Store": "177,175", + "src/textual/widgets/_markdown.py::_task::Attribute::Load": "69,74,76,75", + "src/textual/widgets/_markdown.py::_task::Attribute::Store": "58,70,77", + "src/textual/worker.py::_task::Attribute::Load": "420,441,465,436,442,467", + "src/textual/worker.py::_task::Attribute::Store": "187,423", + "tests/animations/test_disabling_animations.py::run_test::Attribute::Load": "27,49,70,109,130,150", + "tests/animations/test_loading_indicator_animation.py::run_test::Attribute::Load": "14,26,38", + "tests/animations/test_progress_bar_animation.py::run_test::Attribute::Load": "21,32,43", + "tests/animations/test_scrolling_animation.py::run_test::Attribute::Load": "22,40,58,75", + "tests/animations/test_switch_animation.py::run_test::Attribute::Load": "19,38,57", + "tests/animations/test_tabs_underline_animation.py::run_test::Attribute::Load": "24,40,56", + "tests/command_palette/test_click_away.py::run_test::Attribute::Load": "22", + "tests/command_palette/test_command_source_environment.py::run_test::Attribute::Load": "33", + "tests/command_palette/test_declare_sources.py::run_test::Attribute::Load": "31,42,63,74,95", + "tests/command_palette/test_discover.py::run_test::Attribute::Load": "30", + "tests/command_palette/test_escaping.py::run_test::Attribute::Load": "22", + "tests/command_palette/test_events.py::run_test::Attribute::Load": "43,49,56,69", + "tests/command_palette/test_interaction.py::run_test::Attribute::Load": "23,33,45", + "tests/command_palette/test_no_results.py::run_test::Attribute::Load": "15", + "tests/command_palette/test_run_on_select.py::run_test::Attribute::Load": "34,54", + "tests/command_palette/test_worker_interference.py::run_test::Attribute::Load": "25,46", + "tests/css/test_css_path_key.py::run_test::Attribute::Load": "48,71,88", + "tests/css/test_css_reloading.py::run_test::Attribute::Load": "51,82", + "tests/css/test_inheritance.py::run_test::Attribute::Load": "35", + "tests/css/test_initial.py::run_test::Attribute::Load": "42,79", + "tests/css/test_nested_css.py::run_test::Attribute::Load": "40,66,89,169", + "tests/css/test_programmatic_style_changes.py::run_test::Attribute::Load": "13,41,92,132,147,183,208", + "tests/css/test_reapply_rules.py::run_test::Attribute::Load": "22", + "tests/css/test_rule_dependent_caches.py::run_test::Attribute::Load": "53", + "tests/css/test_screen_css.py::run_test::Attribute::Load": "94,117,138,159,178,199,220,244,273,302", + "tests/css/test_stylesheet.py::run_test::Attribute::Load": "23,42,66,149,186,216,267", + "tests/css/test_target_selector_matching.py::run_test::Attribute::Load": "129", + "tests/directory_tree/test_change_path.py::run_test::Attribute::Load": "16", + "tests/directory_tree/test_early_show_root.py::run_test::Attribute::Load": "14", + "tests/footer/test_footer.py::run_test::Attribute::Load": "52", + "tests/footer/test_footer.py::wait_for_refresh::Attribute::Load": "56", + "tests/frame_damage_installed_pilot.py::run_test::Attribute::Load": "11", + "tests/input/test_cut_copy_paste.py::run_test::Attribute::Load": "13,26,39", + "tests/input/test_input_clear.py::run_test::Attribute::Load": "11", + "tests/input/test_input_key_modification_actions.py::run_test::Attribute::Load": "26,36,46,55,65,80,90,100,110,120,129,145,156,166,176", + "tests/input/test_input_key_movement_actions.py::run_test::Attribute::Load": "28,36,44,53,62,71,80,89,105,115,131,140,149,168", + "tests/input/test_input_messages.py::run_test::Attribute::Load": "30,36,43,51,58,65,72", + "tests/input/test_input_mouse.py::run_test::Attribute::Load": "63,74", + "tests/input/test_input_properties.py::run_test::Attribute::Load": "28,34,40,49,59,68,84,101", + "tests/input/test_input_replace.py::run_test::Attribute::Load": "31,60", + "tests/input/test_input_restrict.py::run_test::Attribute::Load": "87,117,140,74", + "tests/input/test_input_terminal_cursor.py::run_test::Attribute::Load": "18,25", + "tests/input/test_input_validation.py::run_test::Attribute::Load": "30,49,59,78,89,112,139,166,182,213", + "tests/input/test_select_on_focus.py::run_test::Attribute::Load": "21", + "tests/layouts/test_grid.py::run_test::Attribute::Load": "29", + "tests/layouts/test_grid.py::wait_for_refresh::Attribute::Load": "31", + "tests/layouts/test_horizontal.py::run_test::Attribute::Load": "26", + "tests/listview/test_inherit_listview.py::run_test::Attribute::Load": "39,48,57", + "tests/listview/test_listview_initial_index.py::run_test::Attribute::Load": "40", + "tests/listview/test_listview_navigation.py::run_test::Attribute::Load": "31", + "tests/listview/test_listview_remove_items.py::run_test::Attribute::Load": "16,53,75,102", + "tests/notifications/test_all_levels_notifications.py::run_test::Attribute::Load": "27", + "tests/notifications/test_app_notifications.py::run_test::Attribute::Load": "12,18,26,36,47", + "tests/option_list/test_option_list_create.py::run_test::Attribute::Load": "29,38,53,61,68,77,86,95,116,126,140,163", + "tests/option_list/test_option_list_disabled.py::run_test::Attribute::Load": "31,39,47,57,67,77,87,94,101,108", + "tests/option_list/test_option_list_id_stability.py::run_test::Attribute::Load": "19", + "tests/option_list/test_option_list_mouse_click.py::run_test::Attribute::Load": "30", + "tests/option_list/test_option_list_mouse_hover.py::run_test::Attribute::Load": "23,30,39,48,60", + "tests/option_list/test_option_list_movement.py::run_test::Attribute::Load": "21,27,35,45,52,61,70,77,84,95,102,113,124,138,159,193", + "tests/option_list/test_option_list_option_subclass.py::run_test::Attribute::Load": "27", + "tests/option_list/test_option_messages.py::run_test::Attribute::Load": "39,47,57,68,78,86,97,108", + "tests/option_list/test_option_prompt_replacement.py::run_test::Attribute::Load": "21,28,35,43,51,60,69", + "tests/option_list/test_option_removal.py::run_test::Attribute::Load": "25,36,47,58,69,81,93,100,107", + "tests/select/test_blank_and_clear.py::run_test::Attribute::Load": "16,28,48,61,74", + "tests/select/test_changed_message.py::run_test::Attribute::Load": "22,32,47,63", + "tests/select/test_empty_select.py::run_test::Attribute::Load": "14,25,49,39", + "tests/select/test_prompt.py::run_test::Attribute::Load": "19,43", + "tests/select/test_remove.py::run_test::Attribute::Load": "24", + "tests/select/test_value.py::run_test::Attribute::Load": "26,33,56,71,83,95,108,123,49", + "tests/selection_list/test_over_wide_selections.py::run_test::Attribute::Load": "22", + "tests/selection_list/test_selection_click_checkbox.py::run_test::Attribute::Load": "26,35", + "tests/selection_list/test_selection_list_create.py::run_test::Attribute::Load": "34,43,52,60,77,88,104,119", + "tests/selection_list/test_selection_messages.py::run_test::Attribute::Load": "52,60,70,84,97,120,133,148,161,176,192,202,212", + "tests/selection_list/test_selection_values.py::run_test::Attribute::Load": "20,26,34,42,50,58,69,77", + "tests/shutdown_mode_installed_pilot.py::run_test::Attribute::Load": "27", + "tests/suggester/test_input_suggestions.py::run_test::Attribute::Load": "22,30,38,46,52,60,69,90,97", + "tests/test_animation.py::run_test::Attribute::Load": "26,48,68,85,106,138,177,188,197,209,251,295", + "tests/test_app.py::run_async::Attribute::Load": "397", + "tests/test_app.py::run_test::Attribute::Load": "39,105,124,136,142,171,200,232,253,262,295,319,348,370,427,441,117", + "tests/test_app_focus_blur.py::run_test::Attribute::Load": "19,29,43,55,69", + "tests/test_app_focus_styles.py::run_test::Attribute::Load": "33,57", + "tests/test_await_remove.py::run_test::Attribute::Load": "19", + "tests/test_batch_paint_admission.py::run_test::Attribute::Load": "43,87,112,180,236,258", + "tests/test_binding.py::run_test::Attribute::Load": "120,135", + "tests/test_binding_inheritance.py::run_test::Attribute::Load": "41,67,91,124,153,226,234,272,319,364,403,457,514,595,634", + "tests/test_border_subtitle.py::run_test::Attribute::Load": "15", + "tests/test_box_model_cache_lifetime.py::run_test::Attribute::Load": "11,36,54", + "tests/test_call_x_schedulers.py::run_test::Attribute::Load": "22,44", + "tests/test_closed_presentation_lifetime.py::run_test::Attribute::Load": "34,48", + "tests/test_collapsible.py::run_test::Attribute::Load": "33,46,59,73,86,98,110,134,162,184,206,228,243", + "tests/test_command.py::run_test::Attribute::Load": "58", + "tests/test_compositor.py::run_test::Attribute::Load": "26,99,143,154,185,228,284,303,339,383", + "tests/test_compositor_frame_damage.py::run_test::Attribute::Load": "13,44,98,136,172,214,269", + "tests/test_compositor_horizontal_damage.py::run_test::Attribute::Load": "41", + "tests/test_containers.py::run_test::Attribute::Load": "36,65,82,97,120", + "tests/test_content_switcher.py::run_test::Attribute::Load": "24,34,45,62,80,103,113,97", + "tests/test_count_parameters.py::run_test::Attribute::Load": "101", + "tests/test_data_bind.py::run_test::Attribute::Load": "25,85", + "tests/test_data_table.py::run_test::Attribute::Load": "70,174,185,209,223,253,263,273,282,296,305,324,333,348,359,375,386,418,428,447,456,466,476,494,505,516,525,535,551,559,578,589,604,615,629,637,652,660,675,688,698,709,719,730,756,773,784,795,824,854,879,893,911,934,955,988,1019,1055,1091,1130,1145,1203,1217,1237,1256,1281,1322,1358,1371,1400,1420,1460,1488", + "tests/test_demo.py::run_test::Attribute::Load": "9", + "tests/test_disabled.py::run_test::Attribute::Load": "50,58,67,77,88,143", + "tests/test_display_constraints.py::run_test::Attribute::Load": "10,28,50", + "tests/test_dom.py::run_test::Attribute::Load": "329", + "tests/test_driver.py::run_test::Attribute::Load": "25,61,84,107,130,170", + "tests/test_dynamic_bindings.py::run_test::Attribute::Load": "31", + "tests/test_focus.py::run_test::Attribute::Load": "36,82,96,114,126,138,151,171,189,206,250,281,301,358,379,395,414,445,501,529", + "tests/test_focus_declaration_targets.py::run_test::Attribute::Load": "27,56,84,103", + "tests/test_footer_projection_lifetime.py::run_test::Attribute::Load": "25,68,113,146", + "tests/test_frame_transaction.py::run_test::Attribute::Load": "9", + "tests/test_freeze.py::run_test::Attribute::Load": "25", + "tests/test_gc.py::run_test::Attribute::Load": "37", + "tests/test_getters.py::run_test::Attribute::Load": "27,60", + "tests/test_header.py::run_test::Attribute::Load": "18,36,54,74,92,113,134", + "tests/test_height_independent_arrangement.py::run_test::Attribute::Load": "19,69,85,99,122,137,160,176,220,238,256,281,298,326,343,366,390", + "tests/test_height_independent_measurement.py::run_test::Attribute::Load": "25,48,65,84,115,133,145,168,185,212,233,248", + "tests/test_issue_4248.py::run_test::Attribute::Load": "27", + "tests/test_keymap.py::run_test::Attribute::Load": "46,58,71,112,171,217", + "tests/test_keys.py::run_test::Attribute::Load": "39", + "tests/test_lazy.py::run_test::Attribute::Load": "18,39", + "tests/test_lazy_dom_storage.py::run_test::Attribute::Load": "42", + "tests/test_links.py::run_test::Attribute::Load": "33", + "tests/test_loading.py::run_test::Attribute::Load": "25,54", + "tests/test_log.py::run_test::Attribute::Load": "22", + "tests/test_logger.py::run_test::Attribute::Load": "40", + "tests/test_markdown.py::run_test::Attribute::Load": "92,107,124,141,162,198,217", + "tests/test_markdown_fence_cache.py::run_test::Attribute::Load": "17,42", + "tests/test_markdownviewer.py::run_test::Attribute::Load": "43,67,84", + "tests/test_masked_input.py::run_test::Attribute::Load": "33,52,63,75,100,120,157,167,176,185,201,210,219,195", + "tests/test_message_handling.py::run_test::Attribute::Load": "46", + "tests/test_message_pump.py::run_test::Attribute::Load": "87,119,156,191,211", + "tests/test_message_pump_payload_lifetime.py::run_test::Attribute::Load": "39,56", + "tests/test_modal.py::run_test::Attribute::Load": "90", + "tests/test_modal.py::wait_for_refresh::Attribute::Load": "95", + "tests/test_mount.py::_task::Attribute::Load": "83", + "tests/test_mount.py::run_test::Attribute::Load": "27,136,100", + "tests/test_mount.py::task::Name::Load": "62,62", + "tests/test_mount.py::task::Name::Store": "62", + "tests/test_native_scene_size_publication.py::run_test::Attribute::Load": "32,66,98,137", + "tests/test_node_list.py::run_test::Attribute::Load": "171,229", + "tests/test_on.py::run_test::Attribute::Load": "43,82,147,193,217,265,302,352,371", + "tests/test_overflow_change.py::run_test::Attribute::Load": "14", + "tests/test_paste.py::run_test::Attribute::Load": "14,40", + "tests/test_pilot.py::run_test::Attribute::Load": "58,128,137,221,279,310,339,354,386,393,424,448,74,89,104,118,410", + "tests/test_pointer_capture_order.py::_task::Attribute::Load": "179", + "tests/test_pointer_capture_order.py::run_test::Attribute::Load": "54,106,129,149,170", + "tests/test_progress_bar.py::run_test::Attribute::Load": "155,204", + "tests/test_query.py::run_test::Attribute::Load": "19,256,266,277,290,307,334,349,375,394", + "tests/test_reactive.py::run_test::Attribute::Load": "29,45,71,100,122,140,163,188,207,228,242,264,300,365,401,432,449,469,493,521,558,597,623,649,686,724,763,786,829", + "tests/test_reactive_watch_lifetime.py::_task::Attribute::Load": "51", + "tests/test_reactive_watch_lifetime.py::run_test::Attribute::Load": "28,46,62,88", + "tests/test_reactive_watch_lifetime.py::task::Name::Load": "52,53", + "tests/test_reactive_watch_lifetime.py::task::Name::Store": "51", + "tests/test_real_app_pump_lifetime.py::_process_messages_body::AsyncFunctionDef::declaration": "138", + "tests/test_real_app_pump_lifetime.py::_start_messages::Attribute::Load": "145", + "tests/test_real_app_pump_lifetime.py::_task::Attribute::Load": "87,107,131,154,148,150", + "tests/test_real_app_pump_lifetime.py::run_async::Attribute::Load": "82,103,128,153", + "tests/test_real_app_pump_lifetime.py::task::Attribute::Load": "46,67,98,47,66,66,66,99,124,124,125,139,149", + "tests/test_real_app_pump_lifetime.py::task::Name::Load": "90,133", + "tests/test_real_app_pump_lifetime.py::task::Name::Store": "90,133", + "tests/test_remove_transaction.py::run_test::Attribute::Load": "31,132,143", + "tests/test_removed_scene_ownership.py::run_test::Attribute::Load": "20,58,97,140,161,189,223", + "tests/test_reparent.py::_task::Attribute::Load": "38,38,48,48", + "tests/test_reparent.py::run_test::Attribute::Load": "31,62,92,107,139", + "tests/test_screen_modes.py::run_test::Attribute::Load": "83,90,99,108,115,125,132,141,151,212", + "tests/test_screens.py::run_test::Attribute::Load": "32,158,201,244,273,292,311,329,359,383,416,451,458,502,587,614,487", + "tests/test_selection.py::run_test::Attribute::Load": "44,82,107,134,159,189,245,307,333,375,399,456", + "tests/test_shutdown.py::run_test::Attribute::Load": "17", + "tests/test_signal.py::_task::Attribute::Load": "179", + "tests/test_signal.py::run_test::Attribute::Load": "39,111,131,149,171", + "tests/test_signal.py::task::Attribute::Load": "175", + "tests/test_signal.py::task::Name::Load": "176,177", + "tests/test_signal.py::task::Name::Store": "175", + "tests/test_structural_measurement_revision.py::run_test::Attribute::Load": "13,42", + "tests/test_style_importance.py::run_test::Attribute::Load": "38,49,60,66,75,83,90,100", + "tests/test_style_inheritance.py::run_test::Attribute::Load": "29", + "tests/test_subtree_geometry_cache.py::run_test::Attribute::Load": "18,34", + "tests/test_suspend.py::run_test::Attribute::Load": "11,54", + "tests/test_switch.py::run_test::Attribute::Load": "17", + "tests/test_tabbed_content.py::run_test::Attribute::Load": "25,84,131,154,183,200,220,238,259,284,300,322,348,364,387,422,457,479,494,510,526,542,561,593,615,637,653,665,677,689,712,728,744,763,785,816,842,860,883,910", + "tests/test_tabs.py::run_test::Attribute::Load": "31,43,62,76,97,122,149,174,201,218,257,290,307,330,360,382,422,428,439,456,470,484,507", + "tests/test_test_runner.py::run_test::Attribute::Load": "14", + "tests/test_timer_lifetime.py::_task::Attribute::Load": "43,48", + "tests/test_timer_lifetime.py::run_test::Attribute::Load": "38,53", + "tests/test_timer_lifetime.py::task::Name::Del": "45", + "tests/test_timer_lifetime.py::task::Name::Load": "44", + "tests/test_timer_lifetime.py::task::Name::Store": "43", + "tests/test_tooltips.py::run_test::Attribute::Load": "38,48,58,71,84,97,110", + "tests/test_unmount.py::run_test::Attribute::Load": "39", + "tests/test_viewport_geometry_targets.py::run_test::Attribute::Load": "52,95,141,176,193,213", + "tests/test_viewport_layout.py::run_test::Attribute::Load": "27", + "tests/test_visible.py::run_test::Attribute::Load": "29,71", + "tests/test_widget.py::run_test::Attribute::Load": "54,141,150,157,174,182,189,196,203,211,222,232,295,313,350,394,404,425,461,491,551,587,646,694,714,759,505,521", + "tests/test_widget_child_moving.py::run_test::Attribute::Load": "11,20,29,38,58,76,94,112,119,129,174,185,147,161", + "tests/test_widget_focus_styles.py::run_test::Attribute::Load": "43,81,112", + "tests/test_widget_measurement_commit.py::run_test::Attribute::Load": "29,56,83,94,109,122,136,152,166", + "tests/test_widget_mounting.py::run_test::Attribute::Load": "22,26,40,47,54,61,68,75,82,89,96,102,111,122", + "tests/test_widget_removing.py::run_test::Attribute::Load": "8,21,30,39,52,65,90,109,130,146,162,179,187,203,218", + "tests/test_widget_visibility.py::run_test::Attribute::Load": "29", + "tests/test_win_sleep.py::run_test::Attribute::Load": "35", + "tests/text_area/test_edit_via_api.py::run_test::Attribute::Load": "43,56,66,77,101,123,132,141,149,160,168,177,200,217,243,269,303,332,347,383,393,400,429,477,514,533,549,559", + "tests/text_area/test_edit_via_bindings.py::run_test::Attribute::Load": "41,49,73,86,97,107,129,148,160,182,209,233,268,297,325,354,366,378,407,422,437,461,475,488,518,539,564,583,611", + "tests/text_area/test_editor_state.py::run_test::Attribute::Load": "13,41", + "tests/text_area/test_escape_binding.py::run_test::Attribute::Load": "30,46", + "tests/text_area/test_history.py::run_test::Attribute::Load": "50,61,75,96,138,153,177,191,200,209,243,264,287,303,352,367,390", + "tests/text_area/test_issue_4301.py::run_test::Attribute::Load": "29", + "tests/text_area/test_languages.py::run_test::Attribute::Load": "20,37,47,56,67,85", + "tests/text_area/test_messages.py::run_test::Attribute::Load": "40,55,69,82,96", + "tests/text_area/test_selection.py::run_test::Attribute::Load": "24,31,39,50,60,69,85,100,114,124,140,157,173,192,224,254,271,292,307,327,347", + "tests/text_area/test_selection_bindings.py::run_test::Attribute::Load": "34,43,51,59,78,87,96,106,118,128,139,148,160,170,180,191,204,216,241,251,264,278,296,306", + "tests/text_area/test_setting_themes.py::run_test::Attribute::Load": "17,29,39,48", + "tests/text_area/test_textarea_cut_copy_paste.py::run_test::Attribute::Load": "13,26,39", + "tests/toggles/test_checkbox.py::run_test::Attribute::Load": "25,37", + "tests/toggles/test_labels.py::run_test::Attribute::Load": "17", + "tests/toggles/test_radiobutton.py::run_test::Attribute::Load": "29,45", + "tests/toggles/test_radioset.py::run_test::Attribute::Load": "32,42,51,67,76,90,110,118,135,162", + "tests/tree/test_directory_tree.py::run_test::Attribute::Load": "39,65,98,148,199", + "tests/tree/test_node_refresh.py::run_test::Attribute::Load": "35,41,50,59", + "tests/tree/test_tree_availability.py::run_test::Attribute::Load": "60,76,92,113", + "tests/tree/test_tree_clearing.py::run_test::Attribute::Load": "48,59,70,80,88,99,108", + "tests/tree/test_tree_cursor.py::run_test::Attribute::Load": "46,62,77,91,110,136,158,171,189", + "tests/tree/test_tree_expand_etc.py::run_test::Attribute::Load": "24,35,47,59,72,88", + "tests/tree/test_tree_messages.py::run_test::Attribute::Load": "57,69,81,92,102,112,124,135,146,157,168,239,249", + "tests/workers/test_work_decorator.py::run_test::Attribute::Load": "45,179", + "tests/workers/test_worker.py::_task::Attribute::Load": "173", + "tests/workers/test_worker.py::run_test::Attribute::Load": "25,58,134,155,180,203,219,244,259,117,232", + "tests/workers/test_worker_context.py::run_test::Attribute::Load": "29,73,53", + "tests/workers/test_worker_manager.py::run_test::Attribute::Load": "45,86,123" + } +} diff --git a/evidence/shared-pump-task-lifetime-20261006/caller-task-assertion-refusal.log b/evidence/shared-pump-task-lifetime-20261006/caller-task-assertion-refusal.log new file mode 100644 index 0000000000..43d5d23165 --- /dev/null +++ b/evidence/shared-pump-task-lifetime-20261006/caller-task-assertion-refusal.log @@ -0,0 +1,50 @@ +FF.. [100%] +=================================== FAILURES =================================== +_______ test_run_async_driver_burst_owns_bubbling_pointer_until_terminal _______ +tests/test_real_app_pump_lifetime.py:87: in test_run_async_driver_burst_owns_bubbling_pointer_until_terminal + assert app.startup_task.done() +E AssertionError: assert False +E + where False = () +E + where = cb=[_run_until_complete_cb() at /usr/lib/python3.14/asyncio/base_events.py:181]>.done +E + where cb=[_run_until_complete_cb() at /usr/lib/python3.14/asyncio/base_events.py:181]> = RealEntryApp(title='RealEntryApp', classes={'-dark-mode'}, pseudo_classes={'dark', 'nocolor', 'focus'}).startup_task +_____________ test_run_async_startup_failure_releases_shared_task ______________ +tests/test_real_app_pump_lifetime.py:106: in test_run_async_startup_failure_releases_shared_task + assert app.observed_task.done() +E AssertionError: assert False +E + where False = () +E + where = cb=[_run_until_complete_cb() at /usr/lib/python3.14/asyncio/base_events.py:181]>.done +E + where cb=[_run_until_complete_cb() at /usr/lib/python3.14/asyncio/base_events.py:181]> = FailedStartup(title='FailedStartup', classes={'-dark-mode'}, pseudo_classes={'dark', 'nocolor', 'focus'}).observed_task +----------------------------- Captured stderr call ----------------------------- +╭───────────────────── Traceback (most recent call last) ──────────────────────╮ +│ /home/ts/wt/textual-frame-publication-20261006/tests/test_real_app_pump_life │ +│ time.py:98 in on_mount │ +│ │ +│ 95 │ │ def on_mount(self): │ +│ 96 │ │ │ self.observed_task = self.task │ +│ 97 │ │ │ assert self.task is asyncio.current_task() │ +│ ❱ 98 │ │ │ raise RuntimeError("original startup failure") │ +│ 99 │ │ +│ 100 │ app = FailedStartup() │ +│ 101 │ await asyncio.wait_for(app.run_async(headless=True, size=(20, 4)), │ +│ │ +│ ╭─────────────────────────── locals ────────────────────────────╮ │ +│ │ @py_assert1 = None │ │ +│ │ @py_assert3 = None │ │ +│ │ @py_assert5 = None │ │ +│ │ @py_assert7 = None │ │ +│ │ self = FailedStartup( │ │ +│ │ │ title='FailedStartup', │ │ +│ │ │ classes={'-dark-mode'}, │ │ +│ │ │ pseudo_classes={ │ │ +│ │ │ │ 'dark', │ │ +│ │ │ │ 'nocolor', │ │ +│ │ │ │ 'focus' │ │ +│ │ │ } │ │ +│ │ ) │ │ +│ ╰───────────────────────────────────────────────────────────────╯ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +RuntimeError: original startup failure +=========================== short test summary info ============================ +FAILED tests/test_real_app_pump_lifetime.py::test_run_async_driver_burst_owns_bubbling_pointer_until_terminal +FAILED tests/test_real_app_pump_lifetime.py::test_run_async_startup_failure_releases_shared_task +2 failed, 2 passed in 0.83s diff --git a/evidence/shared-pump-task-lifetime-20261006/collection-refusal.log b/evidence/shared-pump-task-lifetime-20261006/collection-refusal.log new file mode 100644 index 0000000000..fa8451c2a3 --- /dev/null +++ b/evidence/shared-pump-task-lifetime-20261006/collection-refusal.log @@ -0,0 +1,16 @@ + +==================================== ERRORS ==================================== +____________ ERROR collecting tests/test_real_app_pump_lifetime.py _____________ +ImportError while importing test module '/home/ts/wt/textual-frame-publication-20261006/tests/test_real_app_pump_lifetime.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +/usr/lib/python3.14/importlib/__init__.py:88: in import_module + return _bootstrap._gcd_import(name[level:], package, level) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +tests/test_real_app_pump_lifetime.py:10: in + from test_pointer_capture_order import send_pointer +E ModuleNotFoundError: No module named 'test_pointer_capture_order' +=========================== short test summary info ============================ +ERROR tests/test_real_app_pump_lifetime.py +!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +1 error in 0.54s diff --git a/evidence/shared-pump-task-lifetime-20261006/real-entry.log b/evidence/shared-pump-task-lifetime-20261006/real-entry.log new file mode 100644 index 0000000000..f95dbfe7f4 --- /dev/null +++ b/evidence/shared-pump-task-lifetime-20261006/real-entry.log @@ -0,0 +1,2 @@ +.... [100%] +4 passed in 0.69s diff --git a/tests/test_real_app_pump_lifetime.py b/tests/test_real_app_pump_lifetime.py new file mode 100644 index 0000000000..3bb7efedf9 --- /dev/null +++ b/tests/test_real_app_pump_lifetime.py @@ -0,0 +1,154 @@ +import asyncio + +from textual import events +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.geometry import Offset +from textual.message_pump import MessagePump +from textual.widgets import Static + +from tests.test_pointer_capture_order import send_pointer + + +async def test_run_async_driver_burst_owns_bubbling_pointer_until_terminal(): + delivered, root_deliveries, child_tasks = [], [], [] + wheel = asyncio.Event() + + class Handle(Static): + ALLOW_SELECT = False + + def on_mouse_down(self, event): + self.capture_mouse() + delivered.append("down") + + def on_mouse_move(self, event): + delivered.append("move") + + def on_mouse_up(self, event): + self.release_mouse() + delivered.append("up") + + class Body(Static): + def on_mouse_scroll_up(self, event): + delivered.append("wheel") + wheel.set() + + class RealEntryApp(App[str]): + CSS = "Handle {width: 6; height: 3;} Body {width: 20; height: 3;}" + startup_task = None + + def compose(self) -> ComposeResult: + with Horizontal(): + yield Handle("handle") + yield Body("body") + + def on_mount(self): + self.startup_task = self.task + assert self.task is asyncio.current_task() + + def on_mouse_down(self, event): + root_deliveries.append("down") + + def on_mouse_move(self, event): + root_deliveries.append("move") + + def on_mouse_up(self, event): + root_deliveries.append("up") + + def on_mouse_scroll_up(self, event): + root_deliveries.append("wheel") + + app = RealEntryApp() + + async def drive(pilot): + await pilot.pause() + handle, body = app.query_one(Handle), app.query_one(Body) + child_tasks.extend([handle.task, body.task, app.screen.task]) + assert app.task is app.startup_task + down = handle.region.offset + Offset(1, 1) + away = body.region.offset + Offset(3, 1) + send_pointer(app, events.MouseDown, down) + send_pointer(app, events.MouseMove, away) + send_pointer(app, events.MouseUp, away) + send_pointer(app, events.MouseScrollUp, away, button=0) + await asyncio.wait_for(wheel.wait(), 5) + await pilot.pause() + assert app.mouse_captured is None + assert delivered == ["down", "move", "up", "wheel"] + assert root_deliveries == delivered + app.exit("complete") + + run_task = asyncio.create_task( + app.run_async(headless=True, size=(40, 8), auto_pilot=drive) + ) + result = await asyncio.wait_for(run_task, 10) + assert result == "complete" + assert app._exception is None + assert app._task is None + assert app.startup_task is run_task + assert run_task.done() + assert all(task.done() for task in child_tasks) + + +async def test_run_async_startup_failure_releases_shared_task(): + class FailedStartup(App): + observed_task = None + + def on_mount(self): + self.observed_task = self.task + assert self.task is asyncio.current_task() + raise RuntimeError("original startup failure") + + app = FailedStartup() + run_task = asyncio.create_task(app.run_async(headless=True, size=(20, 4))) + await asyncio.wait_for(run_task, 5) + assert isinstance(app._exception, RuntimeError) + assert app._exception.args == ("original startup failure",) + assert app._task is None + assert not app.is_running + assert app.observed_task is run_task + assert run_task.done() + + +async def test_run_async_cancel_releases_pump_and_children(): + child_tasks = [] + + class CancelledEntry(App): + def compose(self) -> ComposeResult: + yield Static("original source") + + app = CancelledEntry() + + async def cancel(pilot): + await pilot.pause() + child_tasks.extend([app.screen.task, app.query_one(Static).task]) + app.task.cancel() + + run_task = asyncio.create_task( + app.run_async(headless=True, size=(20, 4), auto_pilot=cancel) + ) + await asyncio.wait_for(run_task, 5) + assert app._task is None + assert not app.is_running + assert all(task.done() for task in child_tasks) + + +async def test_eager_completed_pump_is_not_reenrolled_after_release(): + class ImmediatePump(MessagePump): + async def _process_messages_body(self, **kwargs): + assert self.task is asyncio.current_task() + + app = App() + + async def drive(pilot): + pump = ImmediatePump() + pump._start_messages() + # run_async enables the real interpreter's eager factory when present. + # On older interpreters, let the scheduled original task enter and exit. + if pump._task is not None: + await pump.task + assert pump._task is None + app.exit() + + await asyncio.wait_for(app.run_async(headless=True, auto_pilot=drive), 5) + assert app._task is None