From 772796041f5b8b6071383e0df8707071d8a8c501 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 9 Oct 2026 15:52:00 +0800 Subject: [PATCH 1/3] Split composite assertions, drop empty decorator parentheses and write unions with the pipe SonarCloud S9073, S9083 and S6546: a failing 'assert a and b' does not say which half failed. Done by script from the reported lines. --- je_auto_control/gui/main_widget.py | 4 +- je_auto_control/gui/window_settings.py | 4 +- .../utils/action_journal/recorder.py | 6 +- je_auto_control/utils/action_journal/store.py | 10 +- .../utils/action_signing/cipher.py | 14 +- .../utils/action_signing/signer.py | 22 +-- je_auto_control/utils/annotate/annotate.py | 2 +- .../utils/ax_tree_walk/ax_tree_walk.py | 4 +- je_auto_control/utils/clipboard/clipboard.py | 6 +- .../utils/codegen/candidate_diff.py | 4 +- je_auto_control/utils/codegen/codegen.py | 4 +- .../utils/codegen/journal_import.py | 10 +- je_auto_control/utils/config_sync/adapters.py | 10 +- je_auto_control/utils/config_sync/assets.py | 10 +- je_auto_control/utils/config_sync/blobs.py | 4 +- je_auto_control/utils/config_sync/outbox.py | 4 +- je_auto_control/utils/config_sync/store.py | 4 +- .../utils/critical_exit/critical_exit.py | 4 +- .../utils/executor/action_executor.py | 20 +-- .../utils/focus_order/focus_order.py | 6 +- .../generate_report/generate_xml_report.py | 4 +- je_auto_control/utils/json/json_file.py | 4 +- .../utils/json_store/json_store.py | 20 +-- .../utils/locale_parse/locale_parse.py | 8 +- .../utils/mcp_server/disclosure.py | 6 +- .../utils/mcp_server/http_transport.py | 6 +- je_auto_control/utils/mcp_server/server.py | 6 +- je_auto_control/utils/multipart/multipart.py | 8 +- je_auto_control/utils/ocr/ocr_engine.py | 20 +-- je_auto_control/utils/quarantine/store.py | 4 +- .../utils/remote_desktop/file_sync.py | 8 +- .../utils/remote_desktop/signaling_server.py | 12 +- .../utils/run_history/history_store.py | 4 +- je_auto_control/utils/self_healing/locator.py | 5 +- .../utils/shell_process/shell_exec.py | 20 +-- je_auto_control/utils/sql/sql_query.py | 2 +- je_auto_control/utils/trash/trash.py | 4 +- je_auto_control/utils/url_canon/url_canon.py | 4 +- .../utils/window_capture/window_capture.py | 8 +- .../core/utils/win32_keypress_check.py | 3 +- je_auto_control/wrapper/auto_control_image.py | 4 +- .../wrapper/auto_control_keyboard.py | 14 +- je_auto_control/wrapper/auto_control_mouse.py | 18 +-- .../wrapper/auto_control_window.py | 6 +- je_auto_control/wrapper/device_frame.py | 4 +- .../flow_control/test_flow_control.py | 4 +- .../flow_control/test_flow_control_audit.py | 8 +- .../headless/test_a11y_window_audit.py | 6 +- test/unit_test/headless/test_accessibility.py | 6 +- .../headless/test_accessibility_linux.py | 2 +- .../headless/test_accessibility_ocr_audit.py | 9 +- .../test_accessibility_windows_threads.py | 27 ++-- .../headless/test_act_in_view_batch.py | 3 +- .../headless/test_act_modes_batch.py | 3 +- .../headless/test_action_effect_batch.py | 3 +- .../headless/test_action_grounding_batch.py | 15 ++- .../unit_test/headless/test_action_journal.py | 36 +++-- .../headless/test_action_journal_artifacts.py | 8 +- .../headless/test_action_journal_gui.py | 41 ++++-- .../test_action_journal_known_secrets.py | 8 +- .../test_action_journal_runner_parents.py | 2 +- .../headless/test_actionability_batch.py | 24 ++-- .../headless/test_adaptive_timeout_batch.py | 3 +- test/unit_test/headless/test_admin_client.py | 7 +- .../headless/test_agent_action_audit.py | 3 +- .../test_agent_append_only_history.py | 3 +- test/unit_test/headless/test_agent_audit.py | 3 +- .../unit_test/headless/test_agent_backends.py | 3 +- test/unit_test/headless/test_agent_batch.py | 9 +- .../headless/test_agent_memory_batch.py | 2 +- .../headless/test_agent_replay_batch.py | 6 +- .../headless/test_agent_trace_batch.py | 3 +- .../headless/test_agent_vlm_audit.py | 12 +- .../unit_test/headless/test_analysis_batch.py | 6 +- .../headless/test_anchor_ordinal_batch.py | 12 +- test/unit_test/headless/test_anomaly_batch.py | 6 +- .../headless/test_anthropic_computer_use.py | 3 +- .../unit_test/headless/test_app_idle_batch.py | 3 +- .../headless/test_approval_testing_batch.py | 6 +- test/unit_test/headless/test_assets_batch.py | 3 +- .../headless/test_audit_chain_audit.py | 6 +- test/unit_test/headless/test_audit_log.py | 5 +- .../headless/test_ax_events_batch.py | 3 +- .../unit_test/headless/test_ax_props_batch.py | 3 +- test/unit_test/headless/test_ax_text_batch.py | 3 +- .../headless/test_ax_tree_walk_batch.py | 6 +- ...ackground_restart_keeps_old_run_stopped.py | 7 +- test/unit_test/headless/test_baggage_batch.py | 9 +- test/unit_test/headless/test_barcode_batch.py | 3 +- .../headless/test_bidi_check_batch.py | 6 +- .../unit_test/headless/test_candidate_diff.py | 36 +++-- .../headless/test_canonical_log_batch.py | 9 +- .../headless/test_capability_probes.py | 21 ++- .../headless/test_change_localize_batch.py | 3 +- test/unit_test/headless/test_chatops_bot.py | 6 +- .../test_chatops_slack_background_run.py | 23 ++-- .../headless/test_chatops_stoppable_run.py | 31 +++-- .../headless/test_chatops_stopped_run.py | 3 +- .../headless/test_checkpoint_batch.py | 6 +- .../unit_test/headless/test_checksum_batch.py | 3 +- .../headless/test_cli_audit_fixes.py | 3 +- .../headless/test_clipboard_files_batch.py | 9 +- .../headless/test_clipboard_formats_batch.py | 3 +- .../headless/test_clipboard_image.py | 12 +- .../test_clipboard_rich_formats_batch.py | 3 +- .../headless/test_cloudevents_batch.py | 6 +- test/unit_test/headless/test_codegen_audit.py | 6 +- .../headless/test_codegen_cli_report_audit.py | 16 ++- .../headless/test_codegen_from_journal.py | 29 ++-- .../headless/test_codegen_robot_check.py | 3 +- .../headless/test_color_match_batch.py | 6 +- .../headless/test_color_region_batch.py | 3 +- .../headless/test_column_layout_batch.py | 18 ++- .../headless/test_compliance_report_batch.py | 4 +- .../headless/test_computer_toolset.py | 12 +- .../headless/test_computer_use_image_tiers.py | 3 +- test/unit_test/headless/test_config_bundle.py | 5 +- .../headless/test_config_redaction_batch.py | 12 +- .../headless/test_config_schema_batch.py | 6 +- test/unit_test/headless/test_config_sync.py | 4 +- .../test_config_sync_backoff_state.py | 38 ++++-- .../test_config_sync_blob_housekeeping.py | 37 +++-- .../headless/test_config_sync_blobs.py | 48 ++++--- .../headless/test_config_sync_convergence.py | 46 +++++-- .../headless/test_config_sync_empty_change.py | 21 ++- .../test_config_sync_join_without_commit.py | 9 +- .../test_config_sync_legacy_origin.py | 9 +- .../test_config_sync_operation_content.py | 6 +- .../headless/test_config_sync_persistence.py | 6 +- .../headless/test_config_sync_sections.py | 3 +- .../headless/test_config_sync_tab_gui.py | 32 +++-- .../test_config_sync_unlisted_device.py | 15 ++- .../headless/test_confusables_batch.py | 6 +- .../headless/test_console_encoding.py | 2 +- .../headless/test_contrast_map_batch.py | 9 +- .../headless/test_control_patterns_batch.py | 3 +- .../headless/test_cookie_jar_batch.py | 9 +- .../headless/test_cookie_jar_rfc6265.py | 6 +- .../headless/test_coordinate_space_batch.py | 9 +- .../headless/test_credential_lease_batch.py | 3 +- .../headless/test_critic_features_batch.py | 6 +- test/unit_test/headless/test_critical_exit.py | 4 +- test/unit_test/headless/test_cron.py | 6 +- .../headless/test_cross_project_contracts.py | 12 +- .../headless/test_cua_action_batch.py | 6 +- .../headless/test_cvd_simulate_batch.py | 3 +- test/unit_test/headless/test_dashboard.py | 2 +- .../headless/test_data_drift_batch.py | 6 +- .../headless/test_data_format_audit.py | 6 +- .../headless/test_data_profile_batch.py | 15 ++- .../headless/test_data_quality_batch.py | 6 +- .../headless/test_dataset_diff_batch.py | 9 +- .../headless/test_dedup_window_batch.py | 3 +- .../test_dependency_security_floors.py | 4 +- test/unit_test/headless/test_devex_batch.py | 3 +- test/unit_test/headless/test_diagnostics.py | 3 +- test/unit_test/headless/test_dotenv_batch.py | 3 +- test/unit_test/headless/test_dotenv_parity.py | 9 +- .../test_drift_profile_lines_audit.py | 6 +- .../headless/test_edge_lines_batch.py | 6 +- .../headless/test_edge_match_batch.py | 6 +- .../headless/test_element_diff_batch.py | 6 +- .../headless/test_element_parse_batch.py | 9 +- .../headless/test_element_proposal_batch.py | 3 +- .../headless/test_element_scoring_batch.py | 6 +- .../headless/test_ensure_state_batch.py | 3 +- .../headless/test_execute_action_keywords.py | 8 +- .../headless/test_execution_scope_threads.py | 3 +- .../headless/test_executor_result_hook.py | 18 ++- test/unit_test/headless/test_executor_stop.py | 8 +- .../headless/test_expect_poll_batch.py | 19 ++- test/unit_test/headless/test_failure_hooks.py | 3 +- .../headless/test_failure_signature_batch.py | 6 +- .../headless/test_feature_flags_batch.py | 9 +- .../headless/test_feature_match_batch.py | 3 +- .../headless/test_field_entry_batch.py | 3 +- .../headless/test_file_assoc_batch.py | 9 +- .../headless/test_file_boundary_policy.py | 30 +++-- .../headless/test_file_drop_batch.py | 6 +- .../headless/test_file_receiver_abort_race.py | 3 +- .../headless/test_file_store_audit.py | 3 +- .../headless/test_file_transfer_audit.py | 18 ++- .../headless/test_flake_cluster_batch.py | 3 +- test/unit_test/headless/test_flakiness.py | 3 +- .../headless/test_flow_editor_files.py | 3 +- .../headless/test_focus_order_batch.py | 3 +- test/unit_test/headless/test_folder_sync.py | 2 +- .../headless/test_folder_sync_same_tick.py | 18 ++- .../headless/test_form_fields_batch.py | 9 +- .../headless/test_gettext_catalog_batch.py | 3 +- .../headless/test_governance_batch.py | 3 +- .../headless/test_grid_locator_batch.py | 15 ++- .../test_grounding_consensus_batch.py | 6 +- .../test_gui_accessibility_offthread.py | 36 +++-- .../headless/test_gui_daemon_thread.py | 6 +- .../test_gui_engine_state_vlm_audit.py | 5 +- .../headless/test_gui_feature_parity.py | 54 +++++--- .../headless/test_gui_form_settings.py | 14 +- .../headless/test_gui_lazy_registry.py | 18 ++- .../headless/test_gui_listener_cleanup.py | 3 +- .../headless/test_gui_numeric_validators.py | 2 +- .../headless/test_gui_offthread_sites.py | 27 ++-- .../headless/test_gui_panel_dispose.py | 51 ++++--- test/unit_test/headless/test_gui_qt_typed.py | 6 +- test/unit_test/headless/test_gui_restyle.py | 74 ++++++---- .../headless/test_gui_script_runs.py | 33 +++-- .../headless/test_gui_shell_state.py | 106 ++++++++++----- .../headless/test_gui_slots_menu_audit.py | 9 +- .../unit_test/headless/test_gui_slow_stops.py | 74 ++++++---- .../headless/test_gui_tab_dispose.py | 42 ++++-- .../headless/test_gui_tab_lifecycle_audit.py | 14 +- .../headless/test_gui_task_lifecycle.py | 127 ++++++++++++------ .../headless/test_gui_test_hygiene.py | 7 +- .../headless/test_gui_weak_callbacks.py | 12 +- .../headless/test_gui_worker_offload_audit.py | 13 +- .../headless/test_gui_worker_threads.py | 3 +- .../headless/test_gui_workspace_navigation.py | 18 ++- .../headless/test_heading_segment_batch.py | 3 +- test/unit_test/headless/test_helm_chart.py | 6 +- .../headless/test_hotkey_backend_win_mac.py | 3 +- .../headless/test_hsv_segment_batch.py | 12 +- .../headless/test_http_cassette_batch.py | 3 +- .../headless/test_http_conditional_batch.py | 6 +- .../headless/test_http_content_batch.py | 3 +- .../headless/test_http_family_audit.py | 3 +- .../headless/test_http_problem_batch.py | 15 ++- .../headless/test_humanized_motion.py | 3 +- test/unit_test/headless/test_i18n_batch.py | 3 +- .../headless/test_icon_classify_batch.py | 3 +- .../headless/test_icons_search_trust_audit.py | 7 +- .../headless/test_idempotency_batch.py | 3 +- .../headless/test_idle_keepawake_batch.py | 6 +- .../headless/test_image_dedup_batch.py | 6 +- .../headless/test_image_helpers_spec_audit.py | 3 +- .../headless/test_image_matching_audit.py | 3 +- .../test_image_ocr_coordinate_contract.py | 3 +- .../headless/test_image_packaging_audit.py | 6 +- .../headless/test_image_quality_batch.py | 12 +- .../headless/test_image_utils_audit.py | 9 +- .../headless/test_ime_state_batch.py | 3 +- .../headless/test_img_histogram_batch.py | 3 +- .../headless/test_input_form_audit.py | 3 +- .../headless/test_input_helpers_audit.py | 3 +- .../headless/test_input_macro_batch.py | 3 +- test/unit_test/headless/test_input_reach.py | 3 +- .../headless/test_input_replay_audit.py | 3 +- .../headless/test_input_wrapper_contract.py | 12 +- .../test_integration_helpers_audit.py | 7 +- .../headless/test_json_contract_batch.py | 3 +- test/unit_test/headless/test_jwt_audit.py | 3 +- .../headless/test_jwt_key_audience.py | 3 +- .../unit_test/headless/test_key_hold_batch.py | 7 +- .../headless/test_keyboard_layout.py | 3 +- .../test_keyboard_release_on_failure.py | 4 +- .../headless/test_layered_config_batch.py | 12 +- .../headless/test_legacy_accessible_batch.py | 6 +- .../headless/test_license_policy_batch.py | 3 +- .../headless/test_link_header_batch.py | 15 ++- .../headless/test_list_format_batch.py | 3 +- .../headless/test_locale_collation_batch.py | 3 +- .../headless/test_locator_chain_batch.py | 9 +- .../headless/test_locator_repair_batch.py | 3 +- .../headless/test_lock_session_batch.py | 6 +- .../headless/test_logging_encoding.py | 2 +- .../headless/test_logging_location.py | 3 +- .../headless/test_logic_engine_audit.py | 3 +- test/unit_test/headless/test_long_run_stop.py | 3 +- .../headless/test_loop_guard_batch.py | 21 ++- .../headless/test_macos_verify_reporting.py | 3 +- .../headless/test_marks_layout_batch.py | 9 +- .../headless/test_masked_match_batch.py | 6 +- .../headless/test_match_autothresh_batch.py | 6 +- .../headless/test_match_ensemble_batch.py | 6 +- .../headless/test_match_origin_audit.py | 3 +- .../headless/test_match_rbac_bundle_audit.py | 3 +- .../headless/test_match_stability_batch.py | 3 +- .../headless/test_match_trust_batch.py | 6 +- .../unit_test/headless/test_mcp_2025_11_25.py | 8 +- .../unit_test/headless/test_mcp_2026_07_28.py | 101 +++++++++----- .../headless/test_mcp_audit_default_sink.py | 11 +- .../headless/test_mcp_discovery_index.py | 3 +- .../test_mcp_envelope_containment_audit.py | 9 +- .../headless/test_mcp_fake_backend.py | 2 +- .../headless/test_mcp_http_audit_fixes.py | 3 +- .../headless/test_mcp_http_sessions.py | 5 +- .../headless/test_mcp_http_transport.py | 2 +- .../headless/test_mcp_path_or_other.py | 24 +++- .../test_mcp_plugin_read_only_declaration.py | 21 ++- .../headless/test_mcp_progressive_sessions.py | 15 ++- .../headless/test_mcp_protocol_negotiation.py | 2 +- .../headless/test_mcp_read_only_every_mode.py | 3 +- .../test_mcp_resource_authorization.py | 5 +- test/unit_test/headless/test_mcp_server.py | 6 +- .../headless/test_mcp_session_owner.py | 37 +++-- .../headless/test_mcp_stop_execution.py | 3 +- .../headless/test_mcp_tool_safety_audit.py | 9 +- .../headless/test_mcp_window_show_refusal.py | 3 +- .../headless/test_media_analysis_audit.py | 9 +- .../headless/test_message_format_batch.py | 3 +- .../headless/test_message_format_icu.py | 3 +- .../headless/test_mobile_app_lifecycle.py | 9 +- .../headless/test_mobile_surface_parity.py | 6 +- .../headless/test_modernization_examples.py | 16 ++- .../test_modernization_integration.py | 27 ++-- .../headless/test_modifier_state_batch.py | 9 +- .../headless/test_monitor_layout_batch.py | 6 +- .../headless/test_motion_regions_batch.py | 6 +- .../headless/test_mouse_path_batch.py | 18 ++- .../headless/test_mouse_relative_batch.py | 3 +- .../headless/test_multipart_batch.py | 3 +- .../headless/test_multipart_rfc2046.py | 3 +- test/unit_test/headless/test_native_batch.py | 9 +- .../unit_test/headless/test_native_control.py | 2 +- .../unit_test/headless/test_near_dup_batch.py | 6 +- .../headless/test_network_edge_audit.py | 3 +- test/unit_test/headless/test_notify.py | 3 +- .../headless/test_notify_channels_batch.py | 9 +- .../headless/test_observability_audit.py | 3 +- .../headless/test_observation_batch.py | 6 +- .../headless/test_observation_delta_batch.py | 12 +- .../headless/test_observation_state_audit.py | 9 +- .../unit_test/headless/test_observer_batch.py | 3 +- test/unit_test/headless/test_office_batch.py | 4 +- test/unit_test/headless/test_ops_batch.py | 9 +- .../headless/test_optimistic_batch.py | 6 +- .../headless/test_otlp_export_batch.py | 3 +- .../unit_test/headless/test_outbound_audit.py | 9 +- test/unit_test/headless/test_outbox_batch.py | 9 +- .../headless/test_parsers_and_codecs_audit.py | 6 +- .../headless/test_patch_query_audit.py | 6 +- test/unit_test/headless/test_pdf.py | 2 +- .../headless/test_percentiles_batch.py | 3 +- .../headless/test_perceptual_diff_batch.py | 9 +- .../unit_test/headless/test_pii_text_batch.py | 3 +- .../headless/test_platform_backend_binding.py | 2 +- .../test_platform_coordinate_contract.py | 3 +- .../unit_test/headless/test_plumbing_audit.py | 3 +- .../headless/test_popup_watchdog_log_once.py | 16 ++- .../headless/test_postcondition_batch.py | 9 +- .../headless/test_preprocess_batch.py | 12 +- .../headless/test_presence_registry.py | 3 +- .../test_private_key_file_permissions.py | 26 ++-- .../headless/test_process_doc_batch.py | 3 +- .../headless/test_process_mining_batch.py | 3 +- test/unit_test/headless/test_profiler.py | 3 +- .../headless/test_provenance_batch.py | 3 +- .../headless/test_pure_utils_audit.py | 12 +- .../headless/test_quarantine_read_errors.py | 3 +- .../unit_test/headless/test_r3_rdusb_usbip.py | 3 +- .../unit_test/headless/test_r3_vision_poll.py | 3 +- .../headless/test_rate_assets_skills_audit.py | 12 +- test/unit_test/headless/test_rbac.py | 6 +- .../headless/test_rbac_deferred_owner.py | 20 +-- .../headless/test_rbac_deferred_paths.py | 14 +- .../headless/test_rbac_mcp_transports.py | 30 +++-- .../headless/test_rbac_server_wiring.py | 12 +- .../headless/test_rbac_user_admin.py | 45 ++++--- .../unit_test/headless/test_rbac_users_gui.py | 35 +++-- .../headless/test_rd_capture_origin.py | 2 +- test/unit_test/headless/test_rd_data_audit.py | 8 +- test/unit_test/headless/test_rd_gui_audit.py | 19 ++- .../headless/test_rd_webrtc_audit.py | 10 +- .../headless/test_readability_batch.py | 3 +- .../headless/test_reading_flow_batch.py | 6 +- .../headless/test_recording_locator_audit.py | 3 +- .../headless/test_recording_media_audit.py | 6 +- .../headless/test_recurrence_batch.py | 4 +- .../headless/test_redaction_audit.py | 6 +- .../headless/test_referential_batch.py | 9 +- .../headless/test_remote_desktop_audio.py | 2 +- .../test_remote_desktop_file_transfer.py | 3 +- .../headless/test_remote_desktop_gui.py | 3 +- .../headless/test_remote_desktop_host_id.py | 3 +- ...t_remote_desktop_host_service_installer.py | 3 +- .../test_remote_desktop_input_dispatch.py | 2 +- .../headless/test_remote_desktop_io.py | 4 +- .../test_remote_desktop_ip_allowlist.py | 6 +- .../test_remote_desktop_jpeg_recorder.py | 4 +- .../test_remote_desktop_pending_viewer.py | 3 +- .../test_remote_desktop_quick_connect.py | 4 +- .../headless/test_remote_desktop_resume.py | 3 +- .../headless/test_remote_desktop_totp.py | 3 +- .../test_remote_desktop_viewer_reconnect.py | 3 +- .../test_remote_desktop_wire_audit.py | 8 +- .../headless/test_remote_execute_failures.py | 16 ++- .../headless/test_remote_input_send_queue.py | 43 ++++-- .../headless/test_remote_registry_owners.py | 76 +++++++---- .../test_remote_registry_owners_gui.py | 74 ++++++---- .../test_remote_stores_and_cleanup.py | 3 +- .../test_repair_quarantine_renewal_audit.py | 6 +- .../headless/test_resilience_audit.py | 3 +- .../headless/test_resource_profiler.py | 3 +- .../headless/test_rest_audit_fixes.py | 6 +- .../unit_test/headless/test_rest_endpoints.py | 14 +- test/unit_test/headless/test_rest_openapi.py | 2 +- .../headless/test_rest_read_data_routes.py | 7 +- .../headless/test_rest_route_sweep.py | 8 +- test/unit_test/headless/test_rest_server.py | 2 +- .../headless/test_result_redaction.py | 16 ++- .../headless/test_retry_budget_batch.py | 3 +- .../headless/test_rich_clipboard_batch.py | 3 +- .../headless/test_rotated_match_batch.py | 6 +- .../unit_test/headless/test_run_diff_batch.py | 13 +- test/unit_test/headless/test_run_history.py | 6 +- .../test_run_history_journal_link_gui.py | 3 +- .../headless/test_runtime_flow_audit.py | 9 +- test/unit_test/headless/test_saga_batch.py | 3 +- .../unit_test/headless/test_saliency_batch.py | 15 ++- test/unit_test/headless/test_sarif_batch.py | 9 +- .../headless/test_scale_detect_batch.py | 9 +- .../headless/test_schema_compat_batch.py | 12 +- .../headless/test_screen_grid_batch.py | 12 +- .../headless/test_screen_state_batch.py | 6 +- .../headless/test_script_builder_audit.py | 6 +- .../test_script_builder_one_time_reveal.py | 106 ++++++++++----- .../test_script_builder_user_commands.py | 18 ++- .../headless/test_scroll_sign_is_portable.py | 2 +- .../headless/test_search_index_batch.py | 6 +- .../headless/test_secret_color_marks_audit.py | 6 +- .../headless/test_secret_ref_batch.py | 11 +- .../headless/test_security_formats_audit.py | 3 +- .../headless/test_selection_view_batch.py | 3 +- .../test_self_healing_comparison_table.py | 17 ++- .../test_self_healing_eval_surfaces.py | 24 ++-- .../headless/test_self_healing_eval_vlm.py | 13 +- .../headless/test_self_healing_evaluation.py | 36 +++-- .../headless/test_self_healing_replay.py | 3 +- .../test_self_healing_template_revision.py | 27 ++-- .../test_self_healing_verification.py | 19 ++- .../headless/test_semantic_recording.py | 3 +- .../headless/test_sequence_gap_batch.py | 15 ++- .../headless/test_server_report_audit.py | 42 +++--- .../headless/test_set_of_marks_batch.py | 3 +- .../headless/test_settle_detector_batch.py | 6 +- .../headless/test_shape_locator_batch.py | 12 +- .../headless/test_shared_json_dict.py | 3 +- .../test_shell_clipboard_usbip_audit.py | 14 +- .../headless/test_shell_open_batch.py | 6 +- .../headless/test_signaling_config_routes.py | 6 +- .../headless/test_signing_execution_roles.py | 3 +- .../headless/test_signing_passphrase_e2e.py | 39 +++--- .../headless/test_signing_socket_e2e.py | 19 +-- test/unit_test/headless/test_slo_batch.py | 3 +- .../headless/test_small_utils_audit.py | 12 +- .../headless/test_smoothing_batch.py | 3 +- .../headless/test_soft_assert_batch.py | 18 ++- test/unit_test/headless/test_sql_steps.py | 2 +- .../headless/test_sqlite_store_audit.py | 3 +- .../headless/test_sse_client_batch.py | 9 +- .../headless/test_ssim_compare_batch.py | 6 +- test/unit_test/headless/test_stats_batch.py | 6 +- .../headless/test_step_repair_batch.py | 9 +- .../headless/test_step_timeline_batch.py | 3 +- .../headless/test_stop_aware_waits.py | 6 +- .../headless/test_subpixel_match_batch.py | 12 +- .../test_supply_chain_formats_audit.py | 6 +- test/unit_test/headless/test_sync_adapters.py | 62 ++++++--- .../headless/test_sync_composite_triggers.py | 15 ++- .../headless/test_sync_echo_wiring.py | 24 ++-- .../headless/test_system_device_audit.py | 9 +- .../headless/test_system_volume_batch.py | 3 +- .../headless/test_table_grid_fill_batch.py | 6 +- .../headless/test_table_pattern_batch.py | 9 +- .../headless/test_text_and_config_audit.py | 6 +- .../headless/test_text_blocks_batch.py | 15 ++- .../headless/test_text_clipboard_audit.py | 3 +- .../headless/test_text_diff_batch.py | 10 +- .../headless/test_text_helpers_spec_audit.py | 6 +- .../headless/test_text_normalize_batch.py | 3 +- .../headless/test_text_processing_audit.py | 3 +- .../headless/test_text_regions_batch.py | 3 +- .../headless/test_text_similarity_batch.py | 3 +- .../headless/test_text_unicode_batch.py | 6 +- .../headless/test_theme_normalize_batch.py | 3 +- .../headless/test_thread_bound_context.py | 3 +- test/unit_test/headless/test_thread_safety.py | 2 +- .../headless/test_timeseries_batch.py | 6 +- test/unit_test/headless/test_tooling_batch.py | 5 +- .../headless/test_trace_context_batch.py | 12 +- .../headless/test_trace_context_w3c.py | 3 +- .../headless/test_trace_format_audit.py | 3 +- .../headless/test_transform_window_batch.py | 3 +- .../test_trigger_engine_resilience.py | 2 +- test/unit_test/headless/test_turn_config.py | 3 +- .../headless/test_tween_drag_batch.py | 9 +- .../headless/test_typing_extras_contract.py | 7 +- .../test_typing_modernization_contract.py | 3 +- .../headless/test_unattended_reliability.py | 3 +- .../headless/test_url_canon_batch.py | 3 +- test/unit_test/headless/test_usb_acl_audit.py | 3 +- .../headless/test_usb_acl_single_file.py | 27 ++-- .../headless/test_usb_agent_locator_audit.py | 6 +- .../headless/test_usb_browser_tab.py | 2 +- test/unit_test/headless/test_usb_devices.py | 3 +- .../headless/test_usb_passthrough.py | 3 +- .../headless/test_usb_passthrough_client.py | 2 +- .../headless/test_usb_passthrough_executor.py | 2 +- .../headless/test_usb_passthrough_rest.py | 11 +- .../headless/test_usb_request_correlation.py | 9 +- .../headless/test_utils_sweep_audit.py | 18 ++- .../headless/test_verify_field_batch.py | 3 +- .../headless/test_virtualized_batch.py | 3 +- .../headless/test_vision_grid_audit.py | 3 +- .../headless/test_visual_match_batch.py | 12 +- .../headless/test_voice_router_batch.py | 3 +- .../headless/test_vuln_scan_batch.py | 3 +- .../headless/test_wait_color_batch.py | 9 +- .../headless/test_wait_gone_batch.py | 6 +- .../headless/test_wait_window_title_batch.py | 6 +- .../headless/test_wayland_backend.py | 3 +- .../headless/test_wayland_recording_paths.py | 9 +- .../headless/test_wayland_worker_cleanup.py | 3 +- .../headless/test_webrtc_file_transfer.py | 6 +- .../headless/test_webrtc_host_channels.py | 3 +- .../test_webrtc_lan_advertise_stop.py | 11 +- .../headless/test_webrtc_media_audit.py | 6 +- .../headless/test_webrtc_slow_stops.py | 40 ++++-- .../headless/test_webrtc_viewer_control.py | 3 +- .../headless/test_window_arrange_batch.py | 6 +- .../headless/test_window_backends.py | 2 +- .../headless/test_window_geometry_batch.py | 3 +- .../headless/test_window_layout_batch.py | 10 +- .../test_window_lifecycle_contract.py | 3 +- test/unit_test/headless/test_window_manage.py | 2 +- .../headless/test_window_system_audit.py | 13 +- .../headless/test_window_zorder_batch.py | 3 +- .../test_windows_layout_key_aliases.py | 3 +- test/unit_test/headless/test_work_queue.py | 5 +- .../headless/test_wrapper_seam_contract.py | 4 +- test/unit_test/headless/test_write_secret.py | 16 ++- 530 files changed, 3382 insertions(+), 1824 deletions(-) diff --git a/je_auto_control/gui/main_widget.py b/je_auto_control/gui/main_widget.py index 2af3c61a2..b002b45c7 100644 --- a/je_auto_control/gui/main_widget.py +++ b/je_auto_control/gui/main_widget.py @@ -1,6 +1,6 @@ """Workspace widget: owns the tab bar and the registry of every tab it can open.""" import json -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from PySide6.QtCore import QTimer, Signal, QObject from PySide6.QtGui import QKeyEvent, Qt @@ -175,7 +175,7 @@ def _build_remote_desktop_tab() -> QWidget: # --- tab registry API ---------------------------------------------------- def _add_tab( - self, key: str, title_key: str, widget: Union[QWidget, WidgetFactory], + self, key: str, title_key: str, widget: QWidget | WidgetFactory, category: str = "core", default_visible: bool = False, actions: MenuActions = (), scrollable: bool = True, ) -> None: diff --git a/je_auto_control/gui/window_settings.py b/je_auto_control/gui/window_settings.py index 7c19666f7..cdb5352b0 100644 --- a/je_auto_control/gui/window_settings.py +++ b/je_auto_control/gui/window_settings.py @@ -18,7 +18,7 @@ import re from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, Mapping, Optional, Tuple, Union +from typing import Any, Dict, Mapping, Optional, Tuple from PySide6.QtCore import QByteArray, QSettings @@ -87,7 +87,7 @@ def _as_bytes(value: Any) -> bytes: class WindowSettings: """Load and save a :class:`WindowState`; a store without a path does neither.""" - def __init__(self, path: Union[str, Path, None] = _UNSET) -> None: + def __init__(self, path: str | Path | None = _UNSET) -> None: resolved = settings_path() if path is _UNSET else path self._path: Optional[Path] = None if resolved is None else Path(resolved) diff --git a/je_auto_control/utils/action_journal/recorder.py b/je_auto_control/utils/action_journal/recorder.py index b15f20e56..46c67b9ec 100644 --- a/je_auto_control/utils/action_journal/recorder.py +++ b/je_auto_control/utils/action_journal/recorder.py @@ -28,7 +28,7 @@ import uuid from pathlib import Path from types import TracebackType -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar from je_auto_control.utils.action_journal.events import ( STATUS_ERROR, STATUS_INCOMPLETE, STATUS_OK, ActionEvent, @@ -280,7 +280,7 @@ def _fail(session: _Session, error: OSError) -> None: _ACTIVE, _LAST = None, session -def step(action: Any) -> Union[_Step, _NullStep]: +def step(action: Any) -> _Step | _NullStep: """A context manager recording ``action``; a shared no-op when journalling is off.""" session = _ACTIVE if session is None: @@ -389,7 +389,7 @@ def carried(*args: Any, **kwargs: Any) -> Any: return carried # type: ignore[return-value] # reason: same call signature as work -def start_action_journal(path: Union[str, Path, None] = None, *, +def start_action_journal(path: str | Path | None = None, *, run_id: Optional[str] = None, session: Optional[str] = None) -> Dict[str, Any]: """Start writing every executed action to the journal at ``path``. diff --git a/je_auto_control/utils/action_journal/store.py b/je_auto_control/utils/action_journal/store.py index a9d81a631..903a3a4ba 100644 --- a/je_auto_control/utils/action_journal/store.py +++ b/je_auto_control/utils/action_journal/store.py @@ -13,7 +13,7 @@ import threading from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple from je_auto_control.utils.action_journal.events import ( RECORD_END, STATUS_ERROR, STATUS_INCOMPLETE, STATUS_OK, ActionEvent, @@ -30,7 +30,7 @@ def default_journal_path() -> Path: class ActionJournal: """Thread-safe append-only journal file. Appending creates it.""" - def __init__(self, path: Union[str, Path, None] = None) -> None: + def __init__(self, path: str | Path | None = None) -> None: self._path = Path(path) if path is not None else default_journal_path() self._lock = threading.Lock() @@ -102,7 +102,7 @@ def _fold(path: Path, run_id: Optional[str]) -> JournalContents: return JournalContents(tuple(events.values()), lines, tuple(torn)) -def load_journal(path: Union[str, Path], *, run_id: Optional[str] = None +def load_journal(path: str | Path, *, run_id: Optional[str] = None ) -> JournalContents: """Read ``path`` with line numbers and the torn lines it skipped. @@ -116,7 +116,7 @@ def load_journal(path: Union[str, Path], *, run_id: Optional[str] = None return _fold(file_path, run_id) -def read_events(path: Union[str, Path], *, run_id: Optional[str] = None +def read_events(path: str | Path, *, run_id: Optional[str] = None ) -> List[ActionEvent]: """The events in the journal at ``path``, in the order they started. @@ -126,7 +126,7 @@ def read_events(path: Union[str, Path], *, run_id: Optional[str] = None return list(load_journal(path, run_id=run_id).events) -def list_journal_runs(path: Union[str, Path]) -> List[Dict[str, Any]]: +def list_journal_runs(path: str | Path) -> List[Dict[str, Any]]: """One summary per run in the journal at ``path``, oldest first.""" runs: Dict[str, Dict[str, Any]] = {} for event in load_journal(path).events: diff --git a/je_auto_control/utils/action_signing/cipher.py b/je_auto_control/utils/action_signing/cipher.py index f1e38988e..e7cc2302a 100644 --- a/je_auto_control/utils/action_signing/cipher.py +++ b/je_auto_control/utils/action_signing/cipher.py @@ -14,7 +14,7 @@ import hashlib import os from pathlib import Path -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.action_signing._key_file import load_or_create_key_file from je_auto_control.utils.exception.exceptions import ( @@ -35,7 +35,7 @@ def _default_key_path() -> Path: # scrypt cost: 2**15 * 8 * 128 bytes = 32 MiB, about 0.1 s per derivation. _SCRYPT_N, _SCRYPT_R, _SCRYPT_P = 2 ** 15, 8, 1 -KeyType = Optional[Union[bytes, str]] +KeyType = Optional[bytes | str] def _fernet_types() -> tuple: @@ -63,7 +63,7 @@ def _persistent_key() -> bytes: _default_key_path(), fernet_cls.generate_key, _FERNET_KEY_LENGTH) -def _passphrase(key: Union[bytes, str]) -> bytes: +def _passphrase(key: bytes | str) -> bytes: raw = key if isinstance(key, bytes) else str(key).encode("utf-8") if not raw: raise AutoControlException("an empty passphrase encrypts nothing") @@ -105,7 +105,7 @@ def _decrypt(blob: bytes, key: KeyType) -> bytes: return fernet_cls(_legacy_key(passphrase)).decrypt(blob) -def encrypt_action_file(path: Union[str, Path], key: KeyType = None) -> str: +def encrypt_action_file(path: str | Path, key: KeyType = None) -> str: """Encrypt the file at ``path`` to ``.enc``; return the enc path.""" target = Path(path) token = _encrypt(target.read_bytes(), key) @@ -115,8 +115,8 @@ def encrypt_action_file(path: Union[str, Path], key: KeyType = None) -> str: return str(enc_path) -def decrypt_action_file(enc_path: Union[str, Path], key: KeyType = None, - output_path: Optional[Union[str, Path]] = None) -> str: +def decrypt_action_file(enc_path: str | Path, key: KeyType = None, + output_path: Optional[str | Path] = None) -> str: """Decrypt ``enc_path`` to a plaintext file; return its path. ``output_path`` defaults to ``enc_path`` with the ``.enc`` suffix @@ -136,7 +136,7 @@ def decrypt_action_file(enc_path: Union[str, Path], key: KeyType = None, return str(out) -def _output_path(enc: Path, output_path: Optional[Union[str, Path]]) -> Path: +def _output_path(enc: Path, output_path: Optional[str | Path]) -> Path: if output_path is not None: return Path(output_path) if enc.name.endswith(_ENC_SUFFIX): diff --git a/je_auto_control/utils/action_signing/signer.py b/je_auto_control/utils/action_signing/signer.py index bef18d5d1..4ace0e926 100644 --- a/je_auto_control/utils/action_signing/signer.py +++ b/je_auto_control/utils/action_signing/signer.py @@ -27,7 +27,7 @@ import os from dataclasses import asdict, dataclass from pathlib import Path -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional from je_auto_control.utils.action_signing import asymmetric from je_auto_control.utils.action_signing._key_file import load_or_create_key_file @@ -49,8 +49,8 @@ def _default_key_path() -> Path: _REQUIRE_ENV = "JE_AUTOCONTROL_REQUIRE_SIGNED_ACTIONS" _KEY_LENGTH = 32 -KeyType = Optional[Union[bytes, str]] -KeyPath = Optional[Union[str, Path]] +KeyType = Optional[bytes | str] +KeyPath = Optional[str | Path] @dataclass(frozen=True) @@ -92,7 +92,7 @@ def _digest(data: bytes, key: bytes) -> str: def _signature_text(data: bytes, key: KeyType, private_key_path: KeyPath, - passphrase: Optional[Union[bytes, str]] = None) -> str: + passphrase: Optional[bytes | str] = None) -> str: """Sign ``data`` with the key pair when one applies, else with HMAC.""" config = action_signing_config() private = private_key_path @@ -108,9 +108,9 @@ def _signature_text(data: bytes, key: KeyType, private_key_path: KeyPath, return _digest(data, _load_or_create_key(key)) -def sign_action_file(path: Union[str, Path], key: KeyType = None, +def sign_action_file(path: str | Path, key: KeyType = None, *, private_key_path: KeyPath = None, - passphrase: Optional[Union[bytes, str]] = None) -> str: + passphrase: Optional[bytes | str] = None) -> str: """Write a signature sidecar for the file at ``path``. With ``private_key_path`` -- or, when no ``key`` is given, the private key @@ -130,7 +130,7 @@ def sign_action_file(path: Union[str, Path], key: KeyType = None, return str(sig_path) -def verify_action_file(path: Union[str, Path], key: KeyType = None, +def verify_action_file(path: str | Path, key: KeyType = None, *, raise_on_fail: bool = False, public_key_path: KeyPath = None) -> VerifyResult: """Verify the action file at ``path`` against its ``.sig`` sidecar. @@ -172,7 +172,7 @@ def _signature_failure(sidecar: str, data: bytes, key: KeyType, return None -def _verify_bytes(path: Union[str, Path], data: bytes, key: KeyType, +def _verify_bytes(path: str | Path, data: bytes, key: KeyType, raise_on_fail: bool, public_key_path: KeyPath = None) -> VerifyResult: """Check ``data`` -- the content of ``path`` -- against its sidecar.""" sig_path = _sig_path(Path(path)) @@ -188,7 +188,7 @@ def _verify_bytes(path: Union[str, Path], data: bytes, key: KeyType, return VerifyResult(str(path), True, "signature valid") -def _fail(path: Union[str, Path], reason: str, +def _fail(path: str | Path, reason: str, raise_on_fail: bool) -> VerifyResult: if raise_on_fail: raise AutoControlSignatureException( @@ -198,7 +198,7 @@ def _fail(path: Union[str, Path], reason: str, return VerifyResult(str(path), False, reason) -def require_signed_actions(path: Union[str, Path], +def require_signed_actions(path: str | Path, key: KeyType = None) -> None: """Verify ``path`` only when signed-action enforcement is enabled. @@ -216,7 +216,7 @@ def signing_required() -> bool: return bool(os.environ.get(_REQUIRE_ENV)) -def read_signed_action_bytes(path: Union[str, Path], key: KeyType = None) -> bytes: +def read_signed_action_bytes(path: str | Path, key: KeyType = None) -> bytes: """Read ``path`` once and, when enforcement is on, verify those bytes. The caller parses what this returns, so the content that ran is the diff --git a/je_auto_control/utils/annotate/annotate.py b/je_auto_control/utils/annotate/annotate.py index f2289c468..0543d1ea7 100644 --- a/je_auto_control/utils/annotate/annotate.py +++ b/je_auto_control/utils/annotate/annotate.py @@ -91,7 +91,7 @@ def _draw_text(draw: ImageDraw.ImageDraw, ann: Dict[str, Any]) -> None: def annotate_screenshot(source: ImageSource, annotations: List[Dict[str, Any]], - output_path: Union[str, Path]) -> str: + output_path: str | Path) -> str: """Draw ``annotations`` onto ``source`` and save the result as PNG. Returns the output path. ``source`` may be a file path, PNG bytes, or a diff --git a/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py b/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py index 3ca424f0e..554b58bb8 100644 --- a/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py +++ b/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py @@ -17,7 +17,7 @@ Pure-stdlib over :class:`AXTreeNode` values; no device or backend access, no ``PySide6``. Compose it on top of any ``dump_accessibility_tree`` output. """ -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.accessibility.tree import AXTreeNode @@ -47,7 +47,7 @@ def control_type_name(control_type: int) -> str: return _CONTROL_TYPE_NAMES.get(cid, f"{_ROLE_PREFIX}{cid}") -def humanize_role(role: Union[str, int]) -> str: +def humanize_role(role: str | int) -> str: """Map a raw UIA role to a friendly name. Accepts an int id (``50000``), a ``"ControlType_50000"`` string, or a bare diff --git a/je_auto_control/utils/clipboard/clipboard.py b/je_auto_control/utils/clipboard/clipboard.py index 07d4e2466..ada8d21e9 100644 --- a/je_auto_control/utils/clipboard/clipboard.py +++ b/je_auto_control/utils/clipboard/clipboard.py @@ -15,7 +15,7 @@ import subprocess # nosec B404 # reason: required for pbcopy/pbpaste/xclip/xsel import sys from io import BytesIO -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.exception.exceptions import AutoControlException @@ -51,7 +51,7 @@ def get_clipboard_image() -> Optional[bytes]: return _linux_get_image() -def _as_png_bytes(image: Union[bytes, bytearray, str, os.PathLike]) -> bytes: +def _as_png_bytes(image: bytes | bytearray | str | os.PathLike) -> bytes: """PNG bytes for either raw bytes or a path to any Pillow-readable file.""" if isinstance(image, (bytes, bytearray)): if not image: @@ -75,7 +75,7 @@ def _as_png_bytes(image: Union[bytes, bytearray, str, os.PathLike]) -> bytes: def set_clipboard_image( - image: Union[bytes, bytearray, str, os.PathLike]) -> None: + image: bytes | bytearray | str | os.PathLike) -> None: """Place an image on the clipboard, from PNG bytes **or** a file path. Both are accepted because both callers are real: the remote-desktop viewer diff --git a/je_auto_control/utils/codegen/candidate_diff.py b/je_auto_control/utils/codegen/candidate_diff.py index ae3049205..1ecd36319 100644 --- a/je_auto_control/utils/codegen/candidate_diff.py +++ b/je_auto_control/utils/codegen/candidate_diff.py @@ -18,7 +18,7 @@ import json from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence from je_auto_control.utils.codegen.journal_import import CandidateScript, JournalImportError from je_auto_control.utils.json.json_file import read_action_json @@ -110,7 +110,7 @@ def diff_candidate(candidate: CandidateScript, *, actions: Optional[Sequence[Any return diff_code(code or "", candidate.code, before_label=before_label, context=context) -def diff_candidate_against_file(candidate: CandidateScript, path: Union[str, Path], *, +def diff_candidate_against_file(candidate: CandidateScript, path: str | Path, *, context: int = 3) -> CandidateDiff: """Diff ``candidate`` against a file: a ``.json`` action file, or any other file as code. diff --git a/je_auto_control/utils/codegen/codegen.py b/je_auto_control/utils/codegen/codegen.py index 589ba9247..f817e85ec 100644 --- a/je_auto_control/utils/codegen/codegen.py +++ b/je_auto_control/utils/codegen/codegen.py @@ -19,7 +19,7 @@ import pprint import re import textwrap -from typing import Any, Dict, Sequence, Tuple, Union +from typing import Any, Dict, Sequence, Tuple from je_auto_control.utils.json.json_file import read_action_json from je_auto_control.utils.json_store.json_store import atomic_write_text @@ -255,7 +255,7 @@ def _check_shapes(actions: list) -> None: } -def generate_code(actions: Union[Sequence, Dict[str, Any]], target: str = "pytest", +def generate_code(actions: Sequence | Dict[str, Any], target: str = "pytest", name: str = "recorded_flow", style: str = "calls", failure_bundle: bool = False) -> str: """Render ``actions`` as source code for ``target`` (pytest/python/robot). diff --git a/je_auto_control/utils/codegen/journal_import.py b/je_auto_control/utils/codegen/journal_import.py index 69e61ea6c..c07d07b63 100644 --- a/je_auto_control/utils/codegen/journal_import.py +++ b/je_auto_control/utils/codegen/journal_import.py @@ -37,7 +37,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import ( - Any, Dict, FrozenSet, List, Mapping, Optional, Sequence, Set, Tuple, Union, + Any, Dict, FrozenSet, List, Mapping, Optional, Sequence, Set, Tuple, ) from je_auto_control.utils.action_journal.events import ( @@ -337,7 +337,7 @@ def _script_name(run_id: str) -> str: return "journal_run_" + re.sub(r"\W+", "_", run_id)[:40] -def generate_candidate_from_log(path: Union[str, Path], *, run_id: str, +def generate_candidate_from_log(path: str | Path, *, run_id: str, target: str = "pytest", style: str = "actions") -> CandidateScript: """Build a reviewable candidate script from run ``run_id`` of a journal. @@ -377,7 +377,7 @@ def generate_candidate_from_log(path: Union[str, Path], *, run_id: str, actions=builder.actions, observed_path_only=builder.observed) -def only_run_id(path: Union[str, Path]) -> str: +def only_run_id(path: str | Path) -> str: """The id of the journal's single run; raise when there is none or several.""" runs = [run["run_id"] for run in list_journal_runs(os.path.realpath(os.fspath(path)))] if len(runs) != 1: @@ -387,8 +387,8 @@ def only_run_id(path: Union[str, Path]) -> str: return runs[0] -def write_candidate(candidate: CandidateScript, output: Union[str, Path], - manifest: Optional[Union[str, Path]] = None) -> Mapping[str, str]: +def write_candidate(candidate: CandidateScript, output: str | Path, + manifest: Optional[str | Path] = None) -> Mapping[str, str]: """Write the candidate's code (and its manifest, when a path is given).""" written = {"output": os.path.realpath(os.fspath(output))} atomic_write_text(written["output"], candidate.code) diff --git a/je_auto_control/utils/config_sync/adapters.py b/je_auto_control/utils/config_sync/adapters.py index 6eb5b23c6..41a3f3283 100644 --- a/je_auto_control/utils/config_sync/adapters.py +++ b/je_auto_control/utils/config_sync/adapters.py @@ -27,7 +27,7 @@ import json from dataclasses import dataclass, field, fields from pathlib import Path, PurePosixPath -from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Union +from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Optional, Set, Tuple from je_auto_control.utils.config_sync.bucket import ConfigSyncError from je_auto_control.utils.config_sync.versions import SyncEntry, SyncOperation @@ -81,7 +81,7 @@ class SyncAdapter: #: Fields that describe this machine only and are never sent. local_fields: ClassVar[Tuple[str, ...]] = () - def __init__(self, origin: str, *, scripts_dir: Union[str, Path, None] = None) -> None: + def __init__(self, origin: str, *, scripts_dir: str | Path | None = None) -> None: if not origin: raise ConfigSyncError("an adapter needs this device's id") self.origin = origin @@ -300,7 +300,7 @@ class ScriptSyncAdapter(SyncAdapter): section = "scripts" - def __init__(self, origin: str, scripts_dir: Union[str, Path]) -> None: + def __init__(self, origin: str, scripts_dir: str | Path) -> None: super().__init__(origin, scripts_dir=scripts_dir) self._root = Path(scripts_dir) #: Scripts left out of the last snapshot, and why. @@ -393,7 +393,7 @@ class HotkeySyncAdapter(SyncAdapter): arms_on_enable = True def __init__(self, origin: str, daemon: Any, *, - scripts_dir: Union[str, Path, None] = None) -> None: + scripts_dir: str | Path | None = None) -> None: super().__init__(origin, scripts_dir=scripts_dir) self._daemon = daemon @@ -459,7 +459,7 @@ class TriggerSyncAdapter(SyncAdapter): arms_on_enable = True def __init__(self, origin: str, engine: Any, *, - scripts_dir: Union[str, Path, None] = None) -> None: + scripts_dir: str | Path | None = None) -> None: super().__init__(origin, scripts_dir=scripts_dir) self._engine = engine #: Triggers left out of the last snapshot, and why. diff --git a/je_auto_control/utils/config_sync/assets.py b/je_auto_control/utils/config_sync/assets.py index 1573cbb37..3ff00918b 100644 --- a/je_auto_control/utils/config_sync/assets.py +++ b/je_auto_control/utils/config_sync/assets.py @@ -17,7 +17,7 @@ import urllib.parse from dataclasses import dataclass, field from pathlib import Path, PurePosixPath -from typing import Any, Dict, Iterable, List, Mapping, Optional, Protocol, Tuple, Union +from typing import Any, Dict, Iterable, List, Mapping, Optional, Protocol, Tuple from je_auto_control.utils.config_sync.bucket import ConfigSyncError from je_auto_control.utils.exception.exceptions import AutoControlException @@ -33,7 +33,7 @@ class AssetSyncError(ConfigSyncError): """An asset could not be read, fetched, verified or written.""" -def file_sha256(path: Union[str, Path]) -> str: +def file_sha256(path: str | Path) -> str: """The SHA-256 of a file's content, read in chunks.""" digest = hashlib.sha256() with open(path, "rb") as handle: @@ -68,7 +68,7 @@ class AssetManifest: assets: Tuple[AssetRef, ...] = () @classmethod - def from_directory(cls, root: Union[str, Path], + def from_directory(cls, root: str | Path, patterns: Iterable[str] = ("*",)) -> "AssetManifest": """List the files under ``root`` matching any of ``patterns`` (recursive).""" base = Path(root) @@ -81,7 +81,7 @@ def from_directory(cls, root: Union[str, Path], return cls(root=base, assets=tuple(found[key] for key in sorted(found))) @classmethod - def from_entries(cls, root: Union[str, Path], + def from_entries(cls, root: str | Path, entries: Mapping[str, Mapping[str, Any]]) -> "AssetManifest": """Build a manifest from ``{relative path: {"sha256": ..., "size": ...}}``.""" refs = [] @@ -129,7 +129,7 @@ class DirectoryAssetTransport: Blobs are named by their hash, so identical files are stored once. """ - def __init__(self, directory: Union[str, Path]) -> None: + def __init__(self, directory: str | Path) -> None: self._directory = Path(directory) def _blob(self, sha256: str) -> Path: diff --git a/je_auto_control/utils/config_sync/blobs.py b/je_auto_control/utils/config_sync/blobs.py index 700de17b8..6c17ffe6d 100644 --- a/je_auto_control/utils/config_sync/blobs.py +++ b/je_auto_control/utils/config_sync/blobs.py @@ -29,7 +29,7 @@ import threading import time from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from je_auto_control.utils.config_sync.bucket import ConfigSyncError @@ -89,7 +89,7 @@ class BlobStore: ``create_app`` does) has no side effect. """ - def __init__(self, root: Union[str, Path, None] = None, *, + def __init__(self, root: str | Path | None = None, *, max_blob_bytes: int = DEFAULT_MAX_BLOB_BYTES, quota_bytes: int = DEFAULT_BLOB_QUOTA_BYTES, max_users: int = DEFAULT_MAX_BLOB_USERS) -> None: diff --git a/je_auto_control/utils/config_sync/outbox.py b/je_auto_control/utils/config_sync/outbox.py index 3653092da..faef09366 100644 --- a/je_auto_control/utils/config_sync/outbox.py +++ b/je_auto_control/utils/config_sync/outbox.py @@ -22,7 +22,7 @@ import time from dataclasses import dataclass from pathlib import Path -from typing import Any, Callable, ContextManager, List, Optional, Sequence, Union +from typing import Any, Callable, ContextManager, List, Optional, Sequence from je_auto_control.utils.config_sync.client import ( ConfigBucket, ConfigSyncError, FullResyncRequired, @@ -86,7 +86,7 @@ class DrainReport: class SyncOutbox: """The pending operations of one account on one sync endpoint.""" - def __init__(self, db_path: Union[str, Path, None] = None, *, account: str, endpoint: str, + def __init__(self, db_path: str | Path | None = None, *, account: str, endpoint: str, base_delay_s: float = DEFAULT_BASE_DELAY_S, max_delay_s: float = DEFAULT_MAX_DELAY_S) -> None: if not account or not endpoint: diff --git a/je_auto_control/utils/config_sync/store.py b/je_auto_control/utils/config_sync/store.py index 1697202cc..2f83749a0 100644 --- a/je_auto_control/utils/config_sync/store.py +++ b/je_auto_control/utils/config_sync/store.py @@ -24,7 +24,7 @@ import json import time from pathlib import Path -from typing import Any, ContextManager, Optional, Union +from typing import Any, ContextManager, Optional from je_auto_control.utils.config_sync.bucket import ( ConfigBucket, ConfigSyncError, OperationMismatchError, @@ -115,7 +115,7 @@ class ConfigStore: a store (as ``create_app`` does) has no side effect on disk. """ - def __init__(self, db_path: Union[str, Path, None] = None, *, + def __init__(self, db_path: str | Path | None = None, *, max_users: int = DEFAULT_MAX_USERS, max_operations: int = DEFAULT_MAX_OPERATIONS) -> None: self._configured_path = db_path diff --git a/je_auto_control/utils/critical_exit/critical_exit.py b/je_auto_control/utils/critical_exit/critical_exit.py index 3177a85b8..076e5682c 100644 --- a/je_auto_control/utils/critical_exit/critical_exit.py +++ b/je_auto_control/utils/critical_exit/critical_exit.py @@ -2,7 +2,7 @@ import signal import threading from threading import Event, Thread -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.logging.logging_instance import autocontrol_logger from je_auto_control.wrapper.auto_control_keyboard import _resolve_keycode @@ -58,7 +58,7 @@ def __init__(self, default_daemon: bool = True): self._stop_event = Event() def set_critical_key(self, - keycode: Optional[Union[int, str]] = None) -> None: + keycode: Optional[int | str] = None) -> None: """ 設定退出鍵 Set critical exit key diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 0ad0568ea..f3b0709ec 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -1,6 +1,6 @@ import threading import types -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from je_auto_control.utils.exception.exception_tags import ( action_is_null_error_message, add_command_exception_error_message, @@ -2408,7 +2408,7 @@ def _decrypt_action_file(enc_path: str, key: Optional[str] = None, def _annotate_screenshot(source: str, - annotations: Union[List[Dict[str, Any]], str], + annotations: List[Dict[str, Any]] | str, output_path: str) -> Dict[str, Any]: """Executor adapter: draw annotations onto a screenshot and save it. @@ -2435,7 +2435,7 @@ def _move_to_trash(path: str) -> Dict[str, Any]: return {"trashed": move_to_trash(path)} -def _read_qr(region: Optional[Union[List[int], str]] = None) -> Dict[str, Any]: +def _read_qr(region: Optional[List[int] | str] = None) -> Dict[str, Any]: """Executor adapter: decode QR codes in a screen region. ``region`` is ``[x1, y1, x2, y2]`` (or a JSON string for the builder); @@ -2515,14 +2515,14 @@ def _save_window_layout(path: Optional[str] = None) -> Dict[str, Any]: return {"count": len(layout), "path": path, "layout": layout} -def _restore_window_layout(layout: Union[List[Dict[str, Any]], str] +def _restore_window_layout(layout: List[Dict[str, Any]] | str ) -> Dict[str, Any]: """Executor adapter: move windows back to a saved layout (list or path).""" from je_auto_control.utils.window_capture import restore_window_layout return {"restored": restore_window_layout(layout)} -def _region_color_stats(region: Optional[Union[List[int], str]] = None, +def _region_color_stats(region: Optional[List[int] | str] = None, buckets: int = 8) -> Dict[str, Any]: """Executor adapter: average + dominant colour of a screen region. @@ -8379,7 +8379,7 @@ def known_commands(self) -> set: """Return the set of all command names the executor recognises.""" return set(self.event_dict.keys()) | set(self._block_commands.keys()) - def unknown_commands_in(self, action_list: Union[list, dict]) -> List[str]: + def unknown_commands_in(self, action_list: list | dict) -> List[str]: """Return the unrecognised command names in ``action_list``, in order. Nothing is executed. Structural problems raise exactly as @@ -8457,7 +8457,7 @@ def _execute_event(self, action: list) -> Any: return event() raise AutoControlActionException(cant_execute_action_error_message + " " + describe_action(action)) - def execute_action(self, action_list: Union[list, dict], + def execute_action(self, action_list: list | dict, raise_on_error: bool = False, _validated: bool = False, dry_run: bool = False, @@ -8502,7 +8502,7 @@ def execute_action(self, action_list: Union[list, dict], _STRICT_BODIES.value = inherited _RUNNING.value = running - def _execute_list(self, action_list: Union[list, dict], raise_on_error: bool, + def _execute_list(self, action_list: list | dict, raise_on_error: bool, _validated: bool, dry_run: bool, step_callback: Optional[Callable[[list], None]], ) -> Dict[str, str]: @@ -8573,7 +8573,7 @@ def _record_unwound_signal(signal: Exception, record: Dict[str, Any], _count_recorded_failure() @staticmethod - def _unwrap_action_list(action_list: Union[list, dict]) -> list: + def _unwrap_action_list(action_list: list | dict) -> list: """Normalise the ``action_list`` argument or raise on invalid input.""" actions: Any = action_list if isinstance(actions, dict): @@ -8703,7 +8703,7 @@ def add_command_to_executor(command_dict: dict) -> None: raise AutoControlAddCommandException(add_command_exception_error_message) -def execute_action(action_list: Union[list, dict], *, +def execute_action(action_list: list | dict, *, raise_on_error: bool = False, dry_run: bool = False, step_callback: Optional[Callable[[list], None]] = None, result_callback: Optional[ResultHook] = None, diff --git a/je_auto_control/utils/focus_order/focus_order.py b/je_auto_control/utils/focus_order/focus_order.py index 7c65d2120..3f5e192ff 100644 --- a/je_auto_control/utils/focus_order/focus_order.py +++ b/je_auto_control/utils/focus_order/focus_order.py @@ -17,7 +17,7 @@ ``accessibility.backends.get_backend()`` seam; the real ``SetFocus`` call lives in the Windows backend. Imports no ``PySide6``. """ -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence from je_auto_control.utils.accessibility.element import AccessibilityElement from je_auto_control.utils.ax_tree_walk import humanize_role @@ -44,7 +44,7 @@ }) -def _role_key(role: Union[str, int]) -> str: +def _role_key(role: str | int) -> str: """``role`` as a lookup key: humanized, AX prefix dropped, lower case, no spaces.""" text = humanize_role(role) if text.startswith("AX") and text[2:3].isupper(): @@ -52,7 +52,7 @@ def _role_key(role: Union[str, int]) -> str: return "".join(text.lower().split()).replace("_", "") -def is_interactive_role(role: Union[str, int]) -> bool: +def is_interactive_role(role: str | int) -> bool: """Return True if ``role`` is one that normally accepts keyboard focus (any platform's spelling).""" return _role_key(role) in _INTERACTIVE_KEYS diff --git a/je_auto_control/utils/generate_report/generate_xml_report.py b/je_auto_control/utils/generate_report/generate_xml_report.py index 4d1e8daee..eee0271b8 100644 --- a/je_auto_control/utils/generate_report/generate_xml_report.py +++ b/je_auto_control/utils/generate_report/generate_xml_report.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Tuple from defusedxml.minidom import parseString # nosec B405 # nosemgrep: python.lang.security.use-defused-xml.use-defused-xml # reason: defusedxml is the safe replacement @@ -9,7 +9,7 @@ from je_auto_control.utils.xml.change_xml_structure.change_xml_structure import dict_to_elements_tree -def generate_xml() -> Tuple[Union[str, bytes], Union[str, bytes]]: +def generate_xml() -> Tuple[str | bytes, str | bytes]: """ Generate XML strings from test records. 從測試紀錄生成 XML 字串 diff --git a/je_auto_control/utils/json/json_file.py b/je_auto_control/utils/json/json_file.py index 71f5bfe6c..bbad7b45b 100644 --- a/je_auto_control/utils/json/json_file.py +++ b/je_auto_control/utils/json/json_file.py @@ -2,7 +2,7 @@ import os from pathlib import Path from threading import Lock -from typing import Dict, List, Union +from typing import Dict, List from je_auto_control.utils.exception.exception_tags import cant_find_json_error_message, cant_save_json_error_message from je_auto_control.utils.exception.exceptions import AutoControlJsonActionException @@ -55,7 +55,7 @@ def read_executable_action_json(json_file_path: str) -> List[List[Dict[str, Dict raise AutoControlJsonActionException(f"{cant_find_json_error_message}: {repr(error)}") from error -def write_action_json(json_save_path: str, action_json: Union[list, dict]) -> None: +def write_action_json(json_save_path: str, action_json: list | dict) -> None: """ Write action JSON file. 寫入動作 JSON 檔案 diff --git a/je_auto_control/utils/json_store/json_store.py b/je_auto_control/utils/json_store/json_store.py index 542237030..6f770e49d 100644 --- a/je_auto_control/utils/json_store/json_store.py +++ b/je_auto_control/utils/json_store/json_store.py @@ -12,14 +12,14 @@ import time from contextlib import contextmanager from pathlib import Path -from typing import Any, Callable, Dict, Iterator, Optional, TypeVar, Union +from typing import Any, Callable, Dict, Iterator, Optional, TypeVar from je_auto_control.utils.logging.logging_instance import autocontrol_logger _Result = TypeVar("_Result") -def atomic_write_text(path: Union[str, Path], text: str, +def atomic_write_text(path: str | Path, text: str, encoding: str = "utf-8") -> None: """Atomically write ``text`` to ``path`` via a sibling temp file + rename. @@ -31,7 +31,7 @@ def atomic_write_text(path: Union[str, Path], text: str, _atomic_write(path, lambda handle_fd: _write_text_fd(handle_fd, text, encoding)) -def atomic_write_bytes(path: Union[str, Path], data: bytes) -> None: +def atomic_write_bytes(path: str | Path, data: bytes) -> None: """Atomically write ``data`` to ``path``, byte for byte. Like :func:`atomic_write_text` -- same temp file, same rename, and on @@ -49,7 +49,7 @@ def _write_text_fd(handle_fd: int, text: str, encoding: str) -> None: handle.write(text) -def _atomic_write(path: Union[str, Path], write: Callable[[int], None]) -> None: +def _atomic_write(path: str | Path, write: Callable[[int], None]) -> None: file_path = Path(path) directory = str(file_path.parent) or "." handle_fd, tmp_name = tempfile.mkstemp( @@ -85,7 +85,7 @@ def _replace(source: str, destination: str) -> None: time.sleep(0.01) -def append_json_line(path: Union[str, Path], line: str) -> None: +def append_json_line(path: str | Path, line: str) -> None: """Append ``line`` and a newline to a JSON-lines file. If the file does not end in a newline -- the last write was cut off -- @@ -102,7 +102,7 @@ def append_json_line(path: Union[str, Path], line: str) -> None: handle.write(line.encode("utf-8") + b"\n") -def read_json_dict(path: Optional[Union[str, Path]]) -> Dict[str, Any]: +def read_json_dict(path: Optional[str | Path]) -> Dict[str, Any]: """Return the JSON object at ``path``, or ``{}`` if missing/unreadable.""" if path is None: return {} @@ -128,7 +128,7 @@ def _read_json_object(path: Path) -> Dict[str, Any]: return data -def write_json_dict(path: Union[str, Path], data: Dict[str, Any]) -> None: +def write_json_dict(path: str | Path, data: Dict[str, Any]) -> None: """Write ``data`` as indented JSON to ``path`` (creating parent dirs).""" file_path = Path(path) file_path.parent.mkdir(parents=True, exist_ok=True) @@ -192,7 +192,7 @@ class SharedJsonDict: locators) where writing back an empty dict would erase the file. """ - def __init__(self, path: Optional[Union[str, Path]], *, + def __init__(self, path: Optional[str | Path], *, strict: bool = False) -> None: self._path = Path(path) if path is not None else None self._memory: Dict[str, Any] = {} @@ -228,7 +228,7 @@ def update(self, mutate: Callable[[Dict[str, Any]], _Result]) -> _Result: return result -def quarantine_file(path: Union[str, Path], label: str, reason: Any) -> Optional[Path]: +def quarantine_file(path: str | Path, label: str, reason: Any) -> Optional[Path]: """Move an unusable store file aside as ``.corrupt-